訳文:async/await SynchronizationContextコンテキストの問題

16294 ワード

async/awaitは、詳細が隠されているため、非同期コードを書きやすくします.これらの詳細の多くはSynchronizationContextにキャプチャされています.これらの非同期コードの動作は、WPF、Winforms、コンソール、ASP.NETなどのコードを実行する環境によって完全に制御されるため、変更される可能性があります.SynchronizationContextの影響を無視しようとすると、デッドロックや競合状況に遭遇する可能性があります.
SynchronizationContextは、タスクの連続的なスケジューリング方法と場所を制御し、さまざまなコンテキストで使用できます.WPFアプリケーションを作成している場合は、Webサイトを構築するか、ASPを使用します.NETのAPIは、特別なSynchronizationContextを使用していることを知っているはずです.
 

SynchronizationContext in a console application


コンソールアプリケーションのコードを見てみましょう.
public class ConsoleApplication
{
    public static void Main()
    {
        Console.WriteLine($"{DateTime.Now.ToString("T")} - Starting");
        var t1 = ExecuteAsync(() => Library.BlockingOperation());
        var t2 = ExecuteAsync(() => Library.BlockingOperation()));
        var t3 = ExecuteAsync(() => Library.BlockingOperation()));
 
        Task.WaitAll(t1, t2, t3);
        Console.WriteLine($"{DateTime.Now.ToString("T")} - Finished");
        Console.ReadKey();
    }
 
    private static async Task ExecuteAsync(Action action)
    {
        // Execute the continuation asynchronously
        await Task.Yield();  // The current thread returns immediately to the caller
                             // of this method and the rest of the code in this method
                             // will be executed asynchronously
 
        action();
 
        Console.WriteLine($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId}");
    }
}

その中でLibrary.BlockingOperation()はサードパーティ製のライブラリで、使用中のスレッドをブロックします.ブロック操作は任意ですが、テストの目的でThreadを使用できます.実装の代わりにSleep(2)を用いた.
プログラムを実行し、次の結果を出力します.16:39:15 - Starting 16:39:17 - Completed task on thread 11 16:39:17 - Completed task on thread 10 16:39:17 - Completed task on thread 9 16:39:17 - Finished
例では、3つのタスクブロックスレッドをしばらく作成します.Task.Yield強制メソッドは非同期であり、この文の後のすべてのコンテンツをスケジューリングすることによって(_continuation_)と呼ばれる)を実行しますが、すぐに呼び出し元に制御権を返します(Task.Yieldは、スケジューラに「処理が完了したので、他のスレッドに実行権を譲ることができます」と通知し、最終的にどのスレッドを呼び出すかは、スケジューラが決定し、次のスケジューラのスレッドが自分自身である可能性があります).出力から分かるように、Task.Yieldのすべての操作は最終的に並列に実行され、総実行時間は2秒しかありません.
 

SynchronizationContext in an ASP.NET application


仮にASP.でNETアプリケーションでこのコードを再利用するには、コードConsoleを使用します.WriteLineからHttpConextに変換する.Response.Writeでいいです.ページの出力が表示されます.
public class HomeController : Controller
{
    public ActionResult Index()
    {
        HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Starting");
        var t1 = ExecuteAsync(() => Library.BlockingOperation()));
        var t2 = ExecuteAsync(() => Library.BlockingOperation()));
        var t3 = ExecuteAsync(() => Library.BlockingOperation()));
 
        Task.WaitAll(t1, t2, t3);
        HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Finished");
 
        return View();
    }
 
    private async Task ExecuteAsync(Action action)
    {
        await Task.Yield();
 
        action();
        HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId}");
    }
}

ブラウザでこのページを起動してもロードされないことがわかります.私たちはデッドロックを導入したようです.では、ここで何が起こったのでしょうか.
デッドロックの原因はコンソールアプリケーションが非同期操作とASPをスケジューリングすることである.NETが違う.コンソールアプリケーションはスレッドプール上のタスクをスケジューリングするだけですが、ASP.NETは、同じHTTPリクエストのすべての非同期タスクが順番に実行されることを確認します.Task.Yieldは残りの作業をキューに入れ、すぐに呼び出し者に制御権を返すのでTaskを実行しています.WaitAllの場合は3つの待機操作があります.Task.WaitAllはブロック操作であり、Taskのようなブロック操作もある.WaitかTask.Resultは、現在のスレッドをブロックします.
ASP.NETは、スレッドをブロックするデッドロックの原因ではなく、オンライン・スレッド・プールでタスクをスケジュールします.ただし、シーケンス実行であるため、実行開始待ち操作は許可されない.起動できない場合は、いつまでも完了できず、ブロックされたスレッドは続行できません.
このスケジューリングメカニズムはSynchronizationContextクラスによって制御されます.タスクを待機するたびに、待機中の操作が完了すると、await文(つまり続行)の後に実行されるすべてのコンテンツが、現在のSynchronizationContext上でスケジュールされます.コンテキストは、タスクをどのように、いつ、どこで実行するかを決定します.静的SynchronizationContextを使用できます.Currentプロパティは現在のコンテキストにアクセスし、そのプロパティの値はawait文の前後で常に同じです.
コンソールアプリケーションでSynchronizationContext.Currentは常に空です.これは、接続がスレッドプール内の任意のアイドルスレッドによって選択できることを意味します.これは、最初の例で動作を並列に実行できる理由です.でも私たちのASPではNETコントローラには、前述の順序での処理を確実にするAspNetSynchronizationContextがあります.
ポイント1:
Taskのようなブロックタスクの同期方法を使用しないでください.Result,Task.Wait,Task.WaitAllまたはTask.WaitAny. コンソール・アプリケーションのMainメソッドは現在、このルールの唯一の例外です(完全な非同期を取得すると動作が変化するため).
 

ソリューション


Taskを使うべきではないことを知っていますWaitAll、コントローラのIndex Actionを修復しましょう.
public async Task Index()
{
    HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Starting 
");
    var t1 = ExecuteAsync(() => Library.BlockingOperation()));
    var t2 = ExecuteAsync(() => Library.BlockingOperation()));
    var t3 = ExecuteAsync(() => Library.BlockingOperation()));
 
    await Task.WhenAll(t1, t2, t3);
    HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Finished 
");
 
    return View();
}

私たちはTask.WaitAll(t 1,t 2,t 3)は非ブロック待ちTaskに変更する.WhenAll(t 1,t 2,t 3)では、メソッドの戻りタイプをActionResultからasyncタスクに変更する必要があります.
変更後、次の結果がページに出力されます.16:41:03 - Starting 16:41:05 - Completed task on thread 60 16:41:07 - Completed task on thread 50 16:41:09 - Completed task on thread 74 16:41:09 - Finished
 
これはもっとよく見えますが、もう一つの問題があります.コンソールアプリケーションで2秒ではなく、ページのロードに6秒かかります.出力は、タスクを実行する異なるスレッドを見ることができるため、AspNetSynchronizationContextがオンライン・スレッド・プールでの作業をスケジュールしていることをよく示しています.しかし、このコンテキストの順序の性質のため、並列に動作しません.デッドロックを解決しましたが、レプリケーション・ペースト・コードはコンソール・アプリケーションで使用するよりも効率的ではありません.
ポイント2:
非同期コードが並列に実行されると仮定しないでください.明示的に並列実行に設定しない限り.TaskでRunかTask.Factory.StartNewは、非同期コードを並列に実行するようにスケジューリングします.
 

2回目の試み


新しいルールを使用します.
private async Task ExecuteAsync(Action action)
{
    await Task.Yield();
 
    action();
    HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId} 
");
}

to:
private async Task ExecuteAsync(Action action)
{
    await Task.Run(action);
    HttpContext.Response.Write($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId} 
");
}

Task.Runは、SynchronizationContextなしで所定の動作をオンライン・スレッド・プールでスケジュールする.これは、タスク内で実行されるすべてのコンテンツがSynchronizationContext.Currentはnullに設定されています.その結果、すべてのエンキュー操作は任意のスレッドで自由に選択でき、ASPに従う必要はない.NETコンテキストで指定された順序実行順序.これは、タスクが並列に実行できることを意味します.
注意HttpContextはスレッドが安全ではないので、Taskにいるべきではありません.Runではhtml出力で奇妙な結果を生じる可能性があるためアクセスする.しかし、コンテキストキャプチャのため、Response.Writeは、AspNetSynchronizationContext(これはawaitより前の現在のコンテキスト)で発生していることを確認し、HttpContextへのシーケンス化アクセスを確保します.
今回の出力結果は、16:42:27 - Starting 16:42:29 - Completed task on thread 9 16:42:29 - Completed task on thread 12 16:42:29 - Completed task on thread 14 16:42:29 - Finished
 

それだけじゃなく


SynchronizationContextでできることは、スケジューリングタスクだけではありません.AspNetSynchronizationContextはまた、現在実行中のスレッド(スレッドプール全体で作業をスケジュールしていることを覚えておいてください)に正しいユーザが設定されていることを確認し、HttpContext.Currentが使用可能です.
ControllerのHttpContextプロパティを使用できるため、私たちのコードではこれらは必要ありません.非常に有用なExecuteAsyncをヘルプクラスに抽出したい場合は、次のようになります.
class AsyncHelper
{
    public static async Task ExecuteAsync(Action action)
    {
        await Task.Run(action);
        HttpContext.Current.Response.Write($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId} 
");
    }
}

HttpContext.Responseを静的に利用可能なHttpContextに変更する.Current.Response . これはまだ動作します.これはAspNetSynchronizationContextのおかげですが、Taskで試してみると.RunでHttpContextにアクセスする.Current、HttpContext.Currentは設定されていません.
 

文脈を忘れる


前述の例で見たように,コンテキストキャプチャは非常に便利である.しかし、多くの場合、continuationのコンテキストを復元する必要はありません.コンテキストキャプチャには代価があります.必要でない場合は、この追加の論理を避けたほうがいいです.ロードされたWebページを直接書き込むのではなく、ログ・フレームワークに切り替えるとします.ヘルプを書き直します.
class AsyncHelper
{
    public static async Task ExecuteAsync(Action action)
    {
        await Task.Run(action);
        Log.Info($"{DateTime.Now.ToString("T")} - Completed task on thread {Thread.CurrentThread.ManagedThreadId}");
    }
}

現在、await文の後、AspNetSynchronizationContextには必要なものがありませんので、ここでは復元しないのが安全です.タスクを待機した後、ConfigureAwait(false)を使用してコンテキストキャプチャを無効にできます.これにより、待機中のタスクが現在のSynchronizationContextの継続をスケジュールすることを示します.私たちはTaskを使っています.Run,コンテキストはnullであるため,接続はスレッドプール上にスケジューリングされる(順序実行制約はない).
ConfigureAwait(false)を使用するときに覚えておく2つの詳細:
  • コンフィギュレーションAwait(false)を使用する場合、コンフィギュレーションが異なるコンテキストで実行されることは保証されません.それは、インフラストラクチャがコンテキストを復元しないことを伝えるだけで、他のものにアクティブに切り替えるのではなく(Task.Runを使用してコンテキストから抜け出したい場合は).
  • コンテキストキャプチャを無効にするのは、ConfigureAwait(false)を使用するawait文のみです.次のawait文(同じメソッドでは、呼び出しメソッドまたは呼び出されたメソッド)では、コンテキストが再キャプチャされ、リカバリされます.コンテキストに依存しないように、すべてのawait文にConfigureAwait(false)を追加する必要があります.

  •  

    TL; DR;


    非同期コードのSynchronizationContextのため、非同期コードの異なる環境での表現は異なる場合があります.しかし、ベストプラクティスに従うと、問題に直面する確率を最小限に抑えることができます.したがって、async/awaitのベストプラクティスに精通し、継続的に使用してください.
     
    原文:Context Matters