先の記事「await と Task.Result によるデッドロック」の続きです。この記事には、先の記事で述べたデッドロックを回避する方法を書きます。
Task.ConfigureAwait(Boolean) メソッドを使って、先の記事で述べた Task.Result プロパティを使ったコードにおけるデッドロックを回避することができます。
Microsoft のドキュメント「非同期プログラミングのベストプラクティス」に以下の説明があります。
"既定では、未完了の Task を待機するときは、現在のコンテキストがキャプチャされ、Task が完了するときのメソッドの再開に使用されます。 ・・・中略・・・ GUI アプリケーションと ASP.NET アプリケーションには、一度に実行するコードを 1 つのチャンクに限定する SynchronizationContext があります。await が完了するときは、キャプチャしたコンテキスト内で async メソッドの残りを実行しようとします。"
初めは Task.Result で待機しているスレッドを await 終了後に使おうとするからデッドロックが起こるのだろうと思っていましたが、それでは ASP.NET のケースが説明できません。ASP.NET で非同期にする目的はスレッドプールのスレッドの有効利用で、await 前までに使っていたスレッドはスレッドプールに戻し、await が完了後の処理はスレッドプールから新たにスレッドを取得して行いますので(ASP.NET の場合 await 前後でスレッドが異なるということ)。
少なくとも ASP.NET の場合、使われるスレッドが await 前後で同じかどうかはデッドロックに関係なく、上に書いた「一度に実行するコードを 1 つのチャンクに限定する」が関係しているようです。
ということは、GUI アプリ / ASP.NET アプリどちらにせよ、await が完了した後の残りの処理を、await で待機する際にキャプチャした「現在のコンテキスト」ではなく、別のコンテキストで行えばデッドロックにはならないはずです。
Microsoft のドキュメント「非同期プログラミングのベストプラクティス」に以下のように書いてあります。
"ConfigureAwait(continueOnCapturedContext: false)
を追加すると、デッドロックが回避されます。この場合、await が完了するとき、スレッドプールのコンテキスト内で async メソッドの残り処理の実行が試みられます。このメソッドは完了でき、返されたタスクを完了するため、デッドロックは発生しません。"
というわけで、ConfigureAwait メソッドを使って以下のコードで試してみました。上の画像を表示したものです。
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsAsyncTest
{
public partial class Form3 : Form
{
public Form3()
{
InitializeComponent();
}
// Task.Result 使用
private void button1_Click(object sender, EventArgs e)
{
label1.Text = "UI スレッド、ID=" +
Thread.CurrentThread.ManagedThreadId;
label2.Text = TimeCosumingMethod().Result;
label1.Text += " / ID=" +
Thread.CurrentThread.ManagedThreadId;
}
// await 使用
private async void button2_Click(object sender, EventArgs e)
{
label1.Text = "UI スレッド、ID=" +
Thread.CurrentThread.ManagedThreadId;
label2.Text = await TimeCosumingMethod();
label1.Text += " / ID=" +
Thread.CurrentThread.ManagedThreadId;
}
private async Task<string> TimeCosumingMethod()
{
await Task.Delay(3000).ConfigureAwait(
continueOnCapturedContext: false);
return "TimeCosumingMethod の戻り値、ID=" +
Thread.CurrentThread.ManagedThreadId;
}
}
}
上のコードのように ConfigureAwait(continueOnCapturedContext: false)
を追加すると、TimeCosumingMethod メソッドで await Task.Delay(3000) が完了した後の残りの処理は、await で待機する際にキャプチャした「現在のコンテキスト」ではなく、スレッドプールのコンテキスト内で実行されるためデッドロックは回避できるということのようです。
上の画像を見てください。UI スレッドの ManagedThreadId(上の画像では ID=1)と TimeCosumingMethod メソッドの残りを実行したスレッド(上の画像では ID=4)が異なっています。
ただし、Task.Result を使う方は UI がフリーズするのは避けられません。一方、await を使う方は UI はフリーズしません(メッセージループは常に動いていて、マウスのクリックなどのユーザーイベントを処理して UI に反映してくれます)。
理由は、Task.Result は UI スレッドをブロックして TimeCosumingMethod メソッドが完了するのを同期的に待機するのに対して、await は呼び出し元に処理を戻して非同期的に待機するからのようです。
@IT の記事「第2回 非同期メソッドの構文」に以下の説明があります。
"await という名前から、「渡した非同期タスクが完了するまで、呼び出し元スレッドをブロックして待機する」というように感じるかもしれないが、実際はそうではない。await 演算子の意味は、「待っているタスクがまだ完了していない場合、メソッドの残りをそのタスクの『継続』として登録して呼び出し元に処理を戻し、タスクが完了したら登録しておいた継続処理を実行すること」なので注意が必要だ。"
Windows Forms アプリでは「Application.Run メソッド」によって UI スレッドで標準のメッセージループの実行が開始されるそうです。
なので、Task.Result を使う方は、UI スレッドがブロックされている間(上のコード例では約 3 秒間)はメッセージループは動いておらず、マウスのクリックなどのユーザーイベントには無反応(UI がフリーズ状態)になるということのようです。