別に TaskCompletionSource でハマったわけではないですが、TaskCompletionSource で作成したタスクを Async/Await で待ったときにハマってしまった。
元々、OPC Automation で非同期入出力が、コマンドに対しイベントで完了を通知していますので、TaskCompletionSource を使って Task にする説明を考えていました。
最初に、チョロット書こうとしていたことを事実を捻じ曲げて説明します。(本当は引数等が違います。) OPC Automation では、AsyncRead に対し完了を AsyncReadComplete イベントで通知し、操作のキャンセルも AsyncCancel に対し AsyncCancelComplete で完了を通知しています。
Task<object> ReadAsync(..., CancellationToken cancelToken)
{
var tcs = new TaskCompletionSource<object>();
// AsyncReadCompleteイベントが発生したら結果をセット
var readComplete = new AsyncReadCompleteEventHandler((o,e) =>
{
tcs.SetResult(e.Value));
});
// AsyncCancelCompleteイベントが発生したらTaskをキャンセル
var cancelComplete = new AsyncCancelCompleteEventHandler((o,e) =>
{
tcs.SetCanceled());
});
// キャンセルされてなければ
if (!cancelToken.IsCancellationRequested)
{
// イベントハンドラを登録
opc.AsyncReadComplete += readComplete;
opc.AsyncCancelComplete += cancelComplete;
// 非同期読み出し
opc.AsyncRead(...);
// キャンセルされた時の処理を登録 (非同期キャンセルを実行)
cancelToken.Register(() => opc.AsyncCancel(...));
// Taskが完了したら(通常の完了、キャンセル、例外発生の何れか)
tcs.Task.ContinueWith(t =>
{
// イベントハンドラの解除
opc.AsyncReadComplete -= readComplete;
opc.AsyncCancelComplete -= cancelComplete;
});
}
else
{
// 既にキャンセルされてたら、opc から読み取りを行わずに直ちにタスクをキャンセルに
tcs.SetCanceld();
}
// タスクを返す
return tcs.Task;
}
使う側は
// 同期
var r = xxx.ReadAsync(..., CancellationToken.None).Result;
// タスクが完了したときの処理(タスク)をつなげる
xxx.ReadAsync(..., CancellationToken.None)
.ContinueWith(t => Console.WriteLine(t.Result));
// await で待つ
var x = await xxx.ReadAsync(..., CancellationToken.None);
ハマったコード
void Main()
{
var tcs = new TaskCompletionSource<object>();
var tt = Task.Run(() =>
{
Console.WriteLine($"t1:{Thread.CurrentThread.ManagedThreadId}");
tcs.Task.Wait();
Console.WriteLine($"t2:{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
Console.WriteLine($"t3:{Thread.CurrentThread.ManagedThreadId}");
});
Thread.Sleep(500); // Task.Run の開始を待つ
Console.WriteLine($"m1:{Thread.CurrentThread.ManagedThreadId}");
tcs.SetResult(null);
Console.WriteLine($"m2:{Thread.CurrentThread.ManagedThreadId}");
tt.Wait();
Console.WriteLine($"m3:{Thread.CurrentThread.ManagedThreadId}");
}
これは、t1, m1, t2, m2, t3, m3 の順に出力され期待した動作をしています。
void Main()
{
var tcs = new TaskCompletionSource<object>();
var tt = Task.Run(async () =>
{
Console.WriteLine($"t1:{Thread.CurrentThread.ManagedThreadId}");
await tcs.Task;
Console.WriteLine($"t2:{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
Console.WriteLine($"t3:{Thread.CurrentThread.ManagedThreadId}");
});
Thread.Sleep(500); // Task.Run の開始を待つ
Console.WriteLine($"m1:{Thread.CurrentThread.ManagedThreadId}");
tcs.SetResult(null);
Console.WriteLine($"m2:{Thread.CurrentThread.ManagedThreadId}");
tt.Wait();
Console.WriteLine($"m3:{Thread.CurrentThread.ManagedThreadId}");
}
tcs.Task を async/await で待つように修正すると、t1, m2, t2, t3, m2, m3 になってしまいます。 これは、await tcs.Task で完了を待った後、メインのスレッドで実行されているからです。
こんなこともあるよということで。
Written with StackEdit.