Taskとawaitの衝突で起きるデッドロックの回避策として、awaitを含んだ非同期メソッドをTaskでくるむことを挙げたけど、非同期メソッド内にUIコントロールに対する処理がある場合は、Task.Runでくるむのはうまくいかない。たとえば、処理の前後にフォームのlabel1の表示を変えるとして…
async Task someAsyncMethod() { label1.Text = "処理開始"; await someAsyncHeavyMethod(); //何か重たい処理の非同期メソッド label1.Text = "処理完了"; }
…みたいなとき、
Task.Run(() => { return someAsyncMethod(); });
とすると
例外がスローされました: ‘System.InvalidOperationException’ (System.Windows.Forms.dll の中)
追加情報:有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール ‘label1’ がアクセスされました。
というエラーになる。これは、UIコントロールは自身のUIスレッドからしかアクセスできないのに、Task.Run内の処理はスレッドプールのスレッドで行われるから。
こういう場合に使えるのがTask.Factory.StartNewで、TaskScheduler引数に現在の同期コンテキストのTaskSchedulerを指定すると、UIスレッドの同期コンテキスト上でTaskを実行できる。(普通はスレッドプール上の同期コンテキストになる。)具体的には、引数にTaskScheduler.FromCurrentSynchronizationContext()を入れる。
Task.Factory.StartNew(() => { return someAsyncMethod(); }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, //デフォルトはNoneだがTask.Runと同じにした TaskScheduler.FromCurrentSynchronizationContext() );
引数が多くなってしまったが、TaskSchedulerを指定できるオーバーロードがこれ(CancellationToken、TaskCreationOptions、TaskSchedulerの3つとも指定)しか用意されてないのでしかたがない。自分に不必要な引数をわざわざ書くのがいやだったら、TaskSchedulerを指定したTaskFactoryオブジェクトを作って
TaskFactory factory = new TaskFactory(TaskScheduler.FromCurrentSynchronizationContext()); factory.StartNew(() => { return someAsyncMethod(); });
という風にすることもできる。(自分はこの形で使ってる。)
ともかくこうしてUIスレッド上から現在の同期コンテキストを指定してTaskを実行すると、awaitが待つTask以外の処理はUIスレッド上で行われるので、label1の更新も問題なく実行される。
あと、ちなみに、Task.RunやTask.Factory.StartNewのラムダ式内のreturnがないとsomeAsyncMethodの完了を待てなくなるので気をつける。(Task.RunやTask.Factory.StartNewが作るTaskはawaitの手前までで完了になってしまう。Task内で非同期メソッドを実行して、その完了を待つ場合は、その非同期メソッドからもTaskを返して、入れ子のTaskにしないといけない。)