デッドロックが発生するコード
以下のコードではThreadをJoinするタイミングでデッドロックが発生する。
- Closingイベント中Threadを終了させるためのフラグをたて、Join()でスレッド終了を待ち受けつつUIスレッドをブロックする
- ThreadがInvoke呼び出しのタイミングでブロックされる。このタイミングでは、ClosingがUIスレッドを占有しているのでInvokeが終わらない
Threadの終了判定のタイミングによっては、Invokeによるデッドロックが起こさずにループを抜けられるので、必ずデッドロックが発生するというわけではない(コード例では発生しやすいようにSleepを入れて調整している)
Form1.cs
using System.Diagnostics;
namespace WinFormsTaskCancel
{
public partial class Form1 : Form
{
bool ThreadExit = false;
Thread Thread1;
static DateTime StartTime = DateTime.Now;
public static void PrintThreadID(string text = "")
{
int id = System.Threading.Thread.CurrentThread.ManagedThreadId;
Debug.WriteLine($"{(StartTime - DateTime.Now).ToString(@"ss\.fff")}:[{id}]{text}");
}
public Form1()
{
InitializeComponent();
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
PrintThreadID("Closing-start");
ThreadExit = true;
Thread1.Join();
PrintThreadID("Closing-end");
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
PrintThreadID("ClosedClosed");
}
private void Form1_Load(object sender, EventArgs e)
{
Thread1 = new Thread(() =>
{
int i = 0;
while (true)
{
if (ThreadExit)
{
break;
}
Thread.Sleep(1000);
var temp = i++;
PrintThreadID("before Invoke");
Invoke(
() =>
{
label1.Text = ($"{temp}");
}
);
PrintThreadID("after Invoke");
}
});
Thread1.Start();
}
}
}
デッドロックの回避方法
InvokeではなくBeginInvokeを使うとよい。BeginInvokeであれば呼び出しタイミングでブロックされないので、UIスレッドが占有されていても問題なく終了判定までたどり着くことができる。
回避1.cs
Thread1 = new Thread(() =>
{
int i = 0;
while (true)
{
if (ThreadExit)
{
break;
}
Thread.Sleep(1000);
var temp = i++;
PrintThreadID("before Invoke");
//InvokeからBeginInvokeに変更するとブロックされないので終了判定にたどり着ける
BeginInvoke(
() =>
{
label1.Text = ($"{temp}");
}
);
PrintThreadID("after Invoke");
}
});
その他のデッドロック回避方法(1)
Joinを呼び出す直前に空のInvoke()を呼び出す。
この時にInvokeの積み残しが消化されるのでThread側のブロックが解消されて、終了判定にたどり着ける。
その他1.cs
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
PrintThreadID("Closing-start");
ThreadExit = true;
Thread.Invoke(()=>{}); //Invokeの積み残しを消化する
Thread1.Join();
PrintThreadID("Closing-end");
}
その他のデッドロック回避方法(2)
Application.DoEvents()を使ってUIスレッドを完全にブロックしない状態にしつつ、スレッドの終了を待ち受ける。
その他2.cs
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
PrintThreadID("Closing-start");
ThreadExit = true;
//UIスレッドを完全にブロックせずにThreadの終了を待ち受ける
while(Thread1.ThreadState != System.Threading.ThreadState.Stopped)
{
Application.DoEvents();
}
//スレッドが終わっているのでJoinで固まらない。
Thread1.Join();
PrintThreadID("Closing-end");
}
うまくいかない対策
Closingを非同期メソッドにした場合、Closing側でUIスレッドがブロックされなくなるのでデッドロックは解消するが、スレッドが完全に終了するまでにForm側のDisposeが進んでしまうという問題がある。FormのDisposeが終わった時点で、Thread内の処理で存在しないリソースへのアクセスが発生する。
対策失敗.cs
private async void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
PrintThreadID("Closing-start");
ThreadExit = true;
//以降が非同期処理に回されるとFormのDisposeが進んでしまう
//Disposeが先に終わるとThread内のInvokeでエラーになる
await Task.Run(() => Thread1.Join());
PrintThreadID("Closing-end");
}