Skip to content

Instantly share code, notes, and snippets.

@peace2048
Created January 21, 2016 08:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save peace2048/d25acc64c562adc1dca4 to your computer and use it in GitHub Desktop.
Save peace2048/d25acc64c562adc1dca4 to your computer and use it in GitHub Desktop.

TaskCompletionSource でハマる

別に 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment