ライブラリコード内のawait
sにConfigureAwait(false)
を使用して、後続のコードが呼び出し側の実行コンテキスト(UIスレッドなど)で実行されないようにすることをお勧めします。同じ理由で、await Task.Run(CpuBoundWork)
の代わりにCpuBoundWork()
を使用する必要があることも理解しています。
ConfigureAwait
を使用した例public async Task<HtmlDocument> LoadPage(Uri address)
{
using (var client = new HttpClient())
using (var httpResponse = await client.GetAsync(address).ConfigureAwait(false))
using (var responseContent = httpResponse.Content)
using (var contentStream = await responseContent.ReadAsStreamAsync().ConfigureAwait(false))
return LoadHtmlDocument(contentStream); //CPU-bound
}
Task.Run
を使用した例public async Task<HtmlDocument> LoadPage(Uri address)
{
using (var client = new HttpClient())
using (var httpResponse = await client.GetAsync(address))
return await Task.Run(async () =>
{
using (var responseContent = httpResponse.Content)
using (var contentStream = await responseContent.ReadAsStreamAsync())
return LoadHtmlDocument(contentStream); //CPU-bound
});
}
これら2つのアプローチの違いは何ですか?
_Task.Run
_と言うときは、CPUの作業が必要であり、時間がかかる可能性があるため、常にスレッドプールスレッドで実行する必要があると言っています。
ConfigureAwait(false)
と言うとき、そのasync
メソッドの残りは元のコンテキストを必要としないと言っています。 ConfigureAwait
は、より最適化のヒントです。 alwaysではなく、継続がスレッドプールスレッドで実行されることを意味しません。
この場合、最初のawait呼び出し(await client.GetAsync(address)
)は、Task.Run
呼び出しの結果と同様に、呼び出し側コンテキストにまだマーシャリングされるため、Task.Run
バージョンには少しオーバーヘッドがあります。
一方、最初の例では、最初のAsync()
メソッドは呼び出しコンテキストへのマーシャリングを必要としないように構成されているため、継続をバックグラウンドスレッドで実行できます。そのため、呼び出し元のコンテキストへのマーシャリングは行われません。
補足説明として、どちらの場合もLoadPage()
はUIスレッドをstill blockできます。これは、await client.GetAsync(address)
がConfigureAwait(false)
に渡すタスクを作成する時間が必要だからです。また、タスクが返される前に、時間のかかる操作が既に開始されている場合があります。
可能な解決策の1つは、 here のSynchronizationContextRemover
を使用することです。
public async Task<HtmlDocument> LoadPage(Uri address)
{
await new SynchronizationContextRemover();
using (var client = new HttpClient())
using (var httpResponse = await client.GetAsync(address))
using (var responseContent = httpResponse.Content)
using (var contentStream = await responseContent.ReadAsStreamAsync())
return LoadHtmlDocument(contentStream); //CPU-bound
}