2011年9月9日金曜日

lockステートメントは同じスレッドをロックしない (C#)

lockステートメントで少しハマったのでメモ。ハマったコードを単純化したサンプルが以下。こんなプログラム作らねーよというツッコミはなしで。

やってることは単純で起動時にファイルを作って、ウインドウがクリックされたらリネームして元に戻す。リネームして戻すところをlockステートメントで排他をかけている。もし処理が同時に走るとリネーム済みなのに再リネームを試みてFileNotFoundになる。そしてリネームした状態で処理を止められるようポップアップを表示している。

この状態でウインドウを連続クリックすると何が起こるだろうか? 私のようなヘボプログラマは排他待ちになりエラーなしで処理されると直感的に思ってしまった。 でも実はエラーになる。
using System;
using System.IO;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
[DllImport("user32.dll", EntryPoint = "MessageBox")]
extern static Int32 MessageBox(Int32 hWnd, string text, string caption, UInt32 type);

public Form1()
{
InitializeComponent();
if(!File.Exists(fromPath) File.Create(fromPath);
if(File.Exists(toPath) File.Delete(toPath);
}

    string fromPath = "aaa.txt";
    string toPath = "bbb.txt";

    void CriticalSection(string title){
lock (this)
{
File.Move(fromPath, toPath);
MessageBox(0, "処理中", title, 0);
File.Move(toPath, fromPath);
}
}

private void Form1_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
CriticalSection("aaaa");
else
CriticalSection("bbbb");
}
}
}

何故か? ヒントはこちら。

lock によって、あるスレッドがクリティカル セクションになっているときは、別のスレッドはコードのクリティカル セクションにはなりません。ロックされたコードを別のスレッドが使おうとすると、オブジェクトが解放されるまで待機 (ブロック) します。

MSDN lock ステートメント (C# リファレンス)


「別のスレッドは」という表現がこの現象を引き起こした正体。別に同じスレッドが同時には動くことはないんだから問題ないと思うのでは? 上のコードを以下のようにちょっと変えて試してみると良い。
              File.Move(fromPath, toPath);
MessageBox(0, ""+Thread.CurrentThread.ManagedThreadId, title, 0);
File.Move(toPath, fromPath);

同じ数字が表示されるでしょ? 要はクリティカルセクション中でスレッドを切り替えるなということ。スレッドなんて切り替えていないって? 何故、メッセージボックスで処理を止めてるのにメインウインドウの描画が更新されているのか考えてみよう。

アンマネージドコードのデバッグで手を抜いて一時的にメッセージボックスを使ったばかりに変なことにはまって無駄に時間をかけてしまった。

1 件のコメント:

  1. Form1()のInitializeComponent();の下2行について、if文のカッコが閉じられなく、aaa.txtが存在しない場合はlockステートメントの中のFile.Move(fromPath, toPath);で、ファイルが別のプロセスで使用されている例外が発生するので下記が正解だと思います。

    if (!File.Exists(fromPath)) File.Create(fromPath).Close();
    if (File.Exists(toPath)) File.Delete(toPath);

    返信削除