ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

HttpClient を使って同期で通信する

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 がお互いに待機していることが原因です。対策としては次のどちらかが施されていれば回避できますが、両方とも行ったほうが良よさそうです。

  1. すべてを async / await で統一して実装する
  2. 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 の非同期処理を正しく理解して使いこなしていく必要はあります。