この問題を説明する最小限のコードを次に示します。
StringBuilder input = new StringBuilder();
void ToUpper()
{
lock (input)
{
while (true)
{
Monitor.Wait(input);
Console.WriteLine(input.ToString().ToUpper());
}
}
}
public void Run()
{
new Thread(ToUpper) { IsBackground = true }.Start();
// "Avoid" the initial race
Thread.Sleep(100);
while (true)
{
lock (input)
{
input.Clear();
input.Append(Console.ReadLine());
Monitor.Pulse(input);
}
// Thread.Sleep(1);
}
}
よく知られている初期の競合状態を無視すると、Pulse と Wait の動作に驚かされます。
これが私が期待したものです:
- 「ToUpper」スレッド呼び出し
Wait
=> モニターの待機キューにプッシュされます - メインスレッド呼び出し
Pulse
=> 「ToUpper」スレッドが待機キューから準備完了キューに「移動」され、ロックをすぐに取得できるようになります lock
メインスレッドは、ステートメントスコープを終了するときにモニターを終了します- メインスレッドがロックの所有権を再登録している間に、「ToUpper」スレッドがロックを取得して入力を処理します。
しかし、2 回に 1 回は「ToUpper」スレッドが入力を処理せず、代わりにメイン スレッドがその処理をすぐに実行します。
ここに私の仮定があります:
Pulse
「ToUpper」スレッドをすぐに「移動」しないため、準備完了キューは空のままです- メインスレッドがロックを解放し、ループし、再びロックを要求する
- 準備完了キューには他に誰もいないため、所有権を取得します
- 後で「ToUpper」スレッドを昇格するためのリクエストが実行され、最終的に準備完了キューにプッシュされることがあります
- メインスレッド
Pulse
をもう一度何もせずにロックを解放します - ループし、ロックを再取得しようとしますが、「ToUpper」スレッドはすでにそこにあります
- 今回は「ToUpper」スレッドがロックを取得し、入力を処理します
- 完了すると、次の信号を待って再びスリープします
- メインスレッドがロックを取得します
この仮定を確認するために、メイン スレッドの熱意を弱め、他のスレッドが機能するようにThread.Sleep(1)
=> を追加することで、この場合はすべて「期待どおりに機能する」ようにしました。
Pulse
つまり、スレッドを待機キューから準備完了キューにすぐにプッシュしない可能性があるという動作に要約されます。
問題は本当にこのレースの可能性から来ているのでしょうか、それとも私が見逃している別の微妙な点がありますか?