ネコのために鐘は鳴る

寺院に住み着くパソコ〇好き

(C#) 同期コンテキストと Task, async/await その2

この記事は前回の記事の続き、および補足です。

前回記事 (C#) 同期コンテキストと Task, async/await - ネコのために鐘は鳴る

UI スレッドへの復帰コスト

前回の記事で上げた GUI ループでのキューの監視は、ループごとにキューにコールバックがPostされていないか確認しています。

しかし、GUIのループは基本的に描画速度に依存し、60FPSを前提とする環境では1ループ最短で 16ms かかります。つまり、キューを確認した直後に非同期からコールバックをPostされた場合、UI スレッドがきちんと回っている場合でも、コールバックを拾うまでに最長 16ms 遅れます。

これは、わざわざコールバックで UI スレッド復帰する必要がない場合、不要なコストです。

UI スレッド復帰しないawait

Taskawaitのコールバックを UI スレッドに戻す必要がない場合、Task.ConfigureAwaitメソッドを使うことで、UI スレッドに復帰せずにそのままコールバックを実行できます。

// ここは UI スレッド
Debug.WriteLine("Start");
var task = await Task.Run(() => 
{
    // ここはワーカースレッド
    Debug.WriteLine("Worker Thread");
}).ConfigureAwait(false);    // スレッド復帰しない
// ここは同じワーカースレッド
Debug.WriteLine("Callback");

TaskのデフォルトではConfigureAwaittrueに設定されており、この場合、同期コンテキストによる通知はSynchronizationContext.Postメソッドによって行われます。(前回記事参照)

Task.ConfigureAwait(false)にした場合、通知はSynchronizationContext.Sendによって行われ、同期的に実行されます。混乱しそうですが、ワーカースレッドから見て同期的ということは、コールバックは同じワーカースレッドで実行されるということです。

つまり、Task中の処理の続きで連続して行われるため、UI スレッド復帰によるコストは発生しません。

ありがちな罠

// ここは UI スレッド
var task1 = await Task.Run(() => 
{
    // ここはワーカースレッド1
}).ConfigureAwait(false);

// ここはワーカースレッド1
var task2 = await Task.Run(() => 
{
    // ここはワーカースレッド2
}).ConfigureAwait(true);

// ***注意***
// ここはワーカースレッド2

上記のコードの場合、一番下の部分はConfigureAwait(true)の後なので UI スレッドのように見えますが、UI スレッドではありません

ならば、task2を呼び出したのはワーカースレッド1なのだから、ここはスレッド復帰してワーカースレッド1なのかというと、それも違います。正解はワーカースレッド2です。

そもそも 非 UI スレッドの同期コンテキストはスレッド復帰を行わない (前回記事参照) ので最後の部分はワーカースレッド2で実行されます。

 

最後の部分で UI スレッドに復帰したい場合はTask.ContinueWithメソッドを使います。

// ここは UI スレッド
var task = await Task.Run(() => 
{
    // ここはワーカースレッド1
}).ContinueWith(() => 
{
    // ここはワーカースレッド1
}).ConfigureAwait(true);

// ここは UI スレッド

説明のためConfigureAwait(true)をつけていますが、もともとtrueなのでなくても動作は同じです。

ライブラリ作成時の注意点

Taskを内部で使って非同期処理を行うようなライブラリを作る場合、awaitを使う場合原則としてConfigureAwait(false)を必ず付ける必要があります。

つけ忘れると、意図しない部分でスレッドが UI スレッドに復帰してしまう上、コンパイルされたライブラリ dll の利用者から見ると、そのライブラリが中でawaitをつかったスレッド復帰をしているかどうかを確認する術がありません。

なので、基本的にライブラリとして他者に使ってもらう前提のコード中のTaskにはConfigureAwait(false)を必ず付ける必要があります。


前回記事 ikorin2.hatenablog.jp