HttpClient はとても使いやすいのですが、async / await の非同期処理のデッドロックにハマることがあります。
デッドロック
次のコードは、WPF におけるデッドロックの例です。
#MainWindow.xaml.cs private void Button_Click(object sender, RoutedEventArgs e) { var result = this.GetPersonAsync().Result; MessageBox.Show("Hello " + result.Name); } private async Task<Person> GetPersonAsync() { using (var client = new HttpClient()) { var response = await client.GetAsync("http://xxx.azurewebsites.net/api/person"); var responseContent = await response.Content.ReadAsStringAsync(); if (String.IsNullOrEmpty(responseContent)) { return null; } return JsonConvert.DeserializeObject<Person>(responseContent); } }
neuecc さんがブログに書かれているように、Result(Wait)と await がお互いに待機していることが原因です。対策としては次のどちらかが施されていれば回避できますが、両方とも行ったほうが良よさそうです。
- すべてを async / await で統一して実装する
- await しているメソッドに ConfigureAwait(false) を実装する
同期版のメソッドを実装する
別のアプローチとして、同期版の GetPerson メソッドを実装してみました。まず、async / await の非同期メソッドを同期処理で呼び出せるヘルパークラスを作ります。こちらのサイトから、そのままコピーしてきました。Unwrap しているところがポイントです。
#AsyncHelper.cs internal static class AsyncHelper { private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default); public static TResult RunSync<TResult>(Func<Task<TResult>> func) { return AsyncHelper._myTaskFactory.StartNew<Task<TResult>>(func).Unwrap<TResult>().GetAwaiter().GetResult(); } public static void RunSync(Func<Task> func) { AsyncHelper._myTaskFactory.StartNew<Task>(func).Unwrap().GetAwaiter().GetResult(); } }
このヘルパークラスを使って同期版の GetPerson メソッドを実装し、ボタンクリックイベントから呼び出します。この方法なら、デッドロックせずに同期で通信することができます。
#MainWindow.xaml.cs private void Button_Click(object sender, RoutedEventArgs e) { var result = this.GetPerson(); MessageBox.Show("Hello " + result.Name); } private Person GetPerson() { return AsyncHelper.RunSync<Person>(() => this.GetPersonAsync()); }
まとめ
非同期メソッドをライブラリで提供している場合、同期メソッドも提供すれば、アプリ側で Result(Wait)することもなく、デッドロックを発生させてしまうことが少なくなると思います。デットロックが起きる可能性はゼロではないので、async / await の非同期処理を正しく理解して使いこなしていく必要はあります。