web-dev-qa-db-ja.com

Async-Task.RunとHttpClient.GetAsyncを待つ

私はc#5の非同期機能を初めて使用します。私はこれら2つの実装の違いを理解しようとしています:

実装1:

private void Start()
{
    foreach(var url in urls)
    {
        ParseHtml(url);
    }
}

private async void ParseHtml(string url)
{
    var query = BuildQuery(url); //BuildQuery is some helper method
    var html = await DownloadHtml(query);
    //...
    MyType parsedItem = ParseHtml(html);
    SaveTypeToDB(parsedItem);
}

private async Task<string> DownloadHtml(string query)
{
    using (var client = new HttpClient())
    try
    {
        var response = await client.GetAsync(query);
        return (await response.Content.ReadAsAsync<string>());
    }
    catch (Exception ex)
    {
        Logger.Error(msg, ex);
        return null;
    }
}

実装2:

private void DoLoop()
{
    foreach(var url in urls)
    {
        Start(url);
    }
}

private async void Start(url)
{
    await Task.Run( () => ParseHtml(url)) ;
}

private void ParseHtml(string url)
{
    var query = BuildQuery(url); //BuildQuery is some helper method
    var html = DownloadHtml(query);
    //...
    MyType parsedItem = ParseHtml(html);
    SaveTypeToDB(parsedItem);
}

private string DownloadHtml(string query)
{
    using (var client = new WebClient())
    {
        try
        {
            return client.DownloadString(query);
        }
        catch (Exception ex)
        {
            Logger.Error(msg, ex);
            return null;
        }
    }
}

コード内のメソッドに必要な「非同期」署名が少なくなるため、2番目の実装を使用したいと思います。 HttpClientクラスを使用することと、新しいタスクを使用して代わりにそれを待つことの利点を理解しようとしていますか?

2つの実装に違いはありますか?

16
vondip

コード内のメソッドに必要な「非同期」署名が少なくなるため、2番目の実装を使用したいと思います。

それは非常に奇妙な正当化のように聞こえます。あなたは基本的に「やや非同期的に」実行しようとしているので、それを明確にしてみませんか?

2つの実装に違いはありますか?

絶対に。 2番目の実装は、WebClient.DownloadStringがブロックしている間、スレッドを拘束し、要求が完了するのを待ちます。最初のバージョンにはブロックされたスレッドがありません-リクエストが終了したときに起動し続けることに依存しています。

さらに、Logger.Error呼び出しを検討してください。非同期バージョンでは、元の呼び出しコードのコンテキストで実行されます。したがって、これがたとえばWindowsフォームUIにある場合は、引き続きUIスレッドを使用し、UI要素などにアクセスできます。2番目のバージョンでは、スレッドプールスレッドで実行するため、必要になります。 UIスレッドにマーシャリングしてUIを更新します。

あなたのasync voidメソッドはほぼ確実にすべきではないasync voidであることに注意してください。イベントハンドラのシグネチャに準拠するために、asyncメソッドがvoidを返すようにする必要があります。それ以外の場合はすべて、Taskを返します。これにより、呼び出し元はタスクが終了したことを確認したり、例外を処理したりできます。

また、非同期にHttpClientを使用する必要はないことに注意してください。代わりにWebClient.DownloadStringTaskAsyncを使用できるため、最終的なメソッドは次のようになります。

private async Task<string> DownloadHtml(string query)
{
    using (var client = new WebClient())
    {
        try
        {
            return await client.DownloadStringTaskAsync(query);
        }
        catch (Exception ex)
        {
            Logger.Error(msg, ex);
            return null;
        }
    }
}
28
Jon Skeet

サーバーアプリケーションの場合、asyncは、ブロックされたスレッドの数を最小限に抑えることです。つまり、スレッドプールの効率を高め、プログラムをより多くのユーザーに拡張できるようにします。

スレッド数を気にする必要がないクライアントアプリケーションの場合、asyncは、I/Oを実行するときにUIをスムーズに実行し続けるための比較的簡単な方法を提供します。

ボンネットの下にあるTask.Runとは大きく異なります。

6
Cory Nelson

UIの応答性を維持するなど、呼び出し元のスレッドに意味のあることがある場合にのみ、非同期処理のメリットが得られます。呼び出し元のスレッドがタスクを開始するだけで、タスクが終了するまで待つだけの場合、プロセスの実行速度は遅くなります。

2番目の実装はタスクを開始しますが、他に何もせずにタスクが完了するのを待ちます。この方法では、メリットはありません。

環境については説明しませんが、レスポンシブを維持する必要のあるUIがある場合は、Start()が非同期として宣言されておらず、待機しないことを除いて、メソッド1の実装は問題ありません。

private async Task StartAsync()
{
    foreach (var url in urls)
    {
        await ParseHtml(url)
    }
}

これは、次のようにイベントハンドラーから呼び出すことができます。

private async void OnButton1_clicked(object sender, ...)
{
   await StartAsync();
}

注:ParseHtmlの前にはawaitがあります。次のhtmlは、前の解析が終了した後に解析されます。ただし、解析は非同期であるため、呼び出し元のスレッド(UIスレッド?)は、ユーザー入力への応答などの他のことを実行できます。

ただし、parseHTML関数を同時に実行できる場合は、次のコードが望ましいでしょう。おそらくより高速です。

private async Task StartAsync()
{
    var tasks = new List<Task>()
    foreach (var url in urls)
    {
        tasks.Add(ParseHtml(url));
    }
    // now you have a sequence of Tasks scheduled to be run, possibly simultaneously.
    // you can do some other processing here
    // once you need to be certain that all tasks are finished await Task.WhenAll(...)
    await Task.WhenAll(tasks);
    // Task.WhenAls(...) returns a Task, hence you can await for it
    // the return of the await is a void
}
  • タスクを返す関数を呼び出すと、タスクの実行中に他のことを続行したり、タスクが終了するのを待ってタスクの結果を使用したりできます。
  • 待つとコードは停止しますが、発信者はタスクが完了するのを待つまで処理を続けます
  • プロシージャが非同期の場合にのみ、プロシージャで待機できます。
  • 非同期関数は、他の非同期関数から、またはTask.Run(()=> ...)を呼び出すか、必要に応じてTask.Factory.StartNew(()=> ...)を呼び出すことによってのみ呼び出すことができます。
  • Voidの代わりに、非同期関数はTaskを返します
  • TResultの代わりに、非同期関数はTask<TResult>を返します
  • 唯一の例外はイベントハンドラーです。非同期として宣言し、voidを返します。
  • タスクを終了する必要がある場合は、タスクを待つだけです。
  • 待機の戻りはTResultです。
1