web-dev-qa-db-ja.com

すべての推移的閉包でConfigureAwait(false)を使用する必要があるのはなぜですか?

私は非同期/待機を学習しており、この記事を読んだ後 非同期コードでブロックしないでください

そして、これは async/awaitは両方ともIO and CPU bound であるメソッドに適しています

@Stephen Clearyの記事にヒントが1つあります。

ConfigureAwait(false)を使用してデッドロックを回避することは危険です。すべてのサードパーティコードとセカンドパーティコードを含む、ブロッキングコードによって呼び出されるすべてのメソッドの推移的クロージャーで、待機ごとにConfigureAwait(false)を使用する必要があります。 ConfigureAwait(false)を使用してデッドロックを回避することは、せいぜい単なるハックです)。

上記に添付したように、投稿のコードに再び登場しました。

public async Task<HtmlDocument> LoadPage(Uri address)
{
    using (var httpResponse = await new HttpClient().GetAsync(address)
        .ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
    using (var responseContent = httpResponse.Content)
    using (var contentStream = await responseContent.ReadAsStreamAsync()
        .ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
        return LoadHtmlDocument(contentStream); //CPU-bound
}

ConfigureAwait(false)を使用するときの私の知識として、残りの非同期メソッドはスレッドプールで実行されます。推移的閉鎖のすべての待機にそれを追加する必要があるのはなぜですか?私自身は、これが私が知っていた正しいバージョンだと思っています。

public async Task<HtmlDocument> LoadPage(Uri address)
{
    using (var httpResponse = await new HttpClient().GetAsync(address)
        .ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
    using (var responseContent = httpResponse.Content)
    using (var contentStream = await responseContent.ReadAsStreamAsync()) //IO-bound
        return LoadHtmlDocument(contentStream); //CPU-bound
}

これは、ブロックの使用におけるConfigureAwait(false)の2回目の使用は無意味であることを意味します。正しい方法を教えてください。前もって感謝します。

24
vietvoquoc

ConfigureAwait(false)を使用するときの私の知識として、残りの非同期メソッドはスレッドプールで実行されます。

近いですが、見落としている重要な警告があります。 ConfigureAwait(false)でタスクを待ってから再開すると、任意のスレッドで再開します。言葉に注意してください"再開するとき。"

何かお見せしましょう:

public async Task<string> GetValueAsync()
{
    return "Cached Value";
}

public async Task Example1()
{
    await this.GetValueAsync().ConfigureAwait(false);
}

Example1awaitを検討してください。 asyncメソッドを待機していますが、そのメソッドは実際には非同期作業を実行しません。 asyncメソッドがawaitを何もしない場合、それは同期的に実行され、awaiterは最初にsuspendedしないため、resumesを実行しません。この例が示すように、ConfigureAwait(false)への呼び出しは不要な場合があります。まったく効果がない場合があります。この例では、Example1を入力したときに使用していたコンテキストが、awaitの後に使用するコンテキストです。

期待していた通りではありませんか?それでも、それはまったく珍しいことではありません。多くのasyncメソッドには、呼び出し側が中断する必要のない高速パスが含まれている場合があります。キャッシュされたリソースの可用性は良い例です(ありがとう、@jakub-dąbek!)が、asyncメソッドが早期に抜け出すかもしれない他の多くの理由があります。多くの場合、メソッドの最初にさまざまな条件をチェックして、不要な作業を回避できるかどうかを確認します。asyncメソッドも同様です。

今度はWPFアプリケーションからの別の例を見てみましょう。

async Task DoSomethingBenignAsync()
{
    await Task.Yield();
}

Task DoSomethingUnexpectedAsync()
{
    var tcs = new TaskCompletionSource<string>();
    Dispatcher.BeginInvoke(Action(() => tcs.SetResult("Done!")));
    return tcs.Task;
}

async Task Example2()
{
    await DoSomethingBenignAsync().ConfigureAwait(false);
    await DoSomethingUnexpectedAsync();
}

Example2をご覧ください。最初のメソッドawaitalwaysは非同期に実行されます。 2番目のawaitに到達するまでに、スレッドプールスレッドで実行されていることがわかります。したがって、2番目の呼び出しでConfigureAwait(false)は必要ありませんか? 間違った名前にAsyncがあり、Taskを返すにもかかわらず、2番目のメソッドはasyncawaitを使用して記述されていません。代わりに、独自のスケジューリングを実行し、TaskCompletionSourceを使用して結果を伝えます。 awaitから再開すると、[1] 結果を提供したスレッド(この場合はWPFのディスパッチャスレッド)で実行されることになります。おっと。

ここで重要なことは、多くの場合、exactly 'awaitable'メソッドが何をするかを知らないことです。 orCongifureAwaitなしで使用すると、予期しない場所で実行される可能性があります。これは、async呼び出しスタックのどのレベルでも発生する可能性があるため、シングルスレッドコンテキストの所有権を誤って取得しないようにする最も確実な方法は、everyawaitConfigureAwait(false)を使用することです。

もちろん、現在のコンテキストでwantを再開する場合もありますが、それで問題ありません。それが表面上のデフォルトの動作の理由です。しかし、genuinely必要ない場合は、デフォルトでConfigureAwait(false)を使用することをお勧めします。これは、特にライブラリコードに当てはまります。ライブラリコードはどこからでも呼び出すことができるので、最小限の驚きの原則に従うのが最善です。つまり、必要ないときに他のスレッドを呼び出し元のコンテキストからロックしないことを意味します。ライブラリコードのすべての場所でConfigureAwait(false)を使用している場合でも、呼び出し元は、必要に応じてtheir元のコンテキストで再開するオプションを使用できます。

[1] この動作は、フレームワークとコンパイラのバージョンによって異なる場合があります。

27
Mike Strobel