web-dev-qa-db-ja.com

Task.Runを正しく使用し、async-awaitを実行したとき

Task.Runを使用する場合、正しいアーキテクチャについてのあなたの意見をお聞かせください。私はWPF .NET 4.5アプリケーション(Caliburn Microフレームワークを使用)で遅延UIを経験しています。

基本的に私はやっています(非常に単純化されたコードスニペット):

public class PageViewModel : IHandle<SomeMessage>
{
   ...

   public async void Handle(SomeMessage message)
   {
      ShowLoadingAnimation();

      // Makes UI very laggy, but still not dead
      await this.contentLoader.LoadContentAsync();

      HideLoadingAnimation();
   }
}

public class ContentLoader
{
    public async Task LoadContentAsync()
    {
        await DoCpuBoundWorkAsync();
        await DoIoBoundWorkAsync();
        await DoCpuBoundWorkAsync();

        // I am not really sure what all I can consider as CPU bound as slowing down the UI
        await DoSomeOtherWorkAsync();
    }
}

私が読んだり見た記事やビデオから、awaitasyncは必ずしもバックグラウンドスレッドで実行されているわけではなく、バックグラウンドで作業を始めるためにはTask.Run(async () => ... )でラップする必要があります。 asyncawaitを使用してもUIはブロックされませんが、まだUIスレッド上で実行されているため、遅れています。

Task.Runを置くのに最適な場所はどこですか?

私だけ

  1. これは.NETのスレッド化作業が少ないため、外部呼び出しをラップします。

  2. それとも他の場所で再利用可能になるのでTask.Runで内部的に実行しているCPUバウンドメソッドだけをラップすべきですか?コアの深いバックグラウンドスレッドで作業を開始するのがよいかどうか、私はここではわかりません。

Ad(1)、最初の解決策は次のようになります。

public async void Handle(SomeMessage message)
{
    ShowLoadingAnimation();
    await Task.Run(async () => await this.contentLoader.LoadContentAsync());
    HideLoadingAnimation();
}

// Other methods do not use Task.Run as everything regardless
// if I/O or CPU bound would now run in the background.

Ad(2)、2番目の解決策は次のようになります。

public async Task DoCpuBoundWorkAsync()
{
    await Task.Run(() => {
        // Do lot of work here
    });
}

public async Task DoSomeOtherWorkAsync(
{
    // I am not sure how to handle this methods -
    // probably need to test one by one, if it is slowing down UI
}
262
Lukas K

私のブログで収集した UIスレッド で作業を実行するためのガイドライン:)に注意してください。

  • 一度に50ミリ秒以上UIスレッドをブロックしないでください。
  • UIスレッドで毎秒最大100の継続をスケジュールすることができます。 1000は多すぎます。

使用すべき2つの手法があります。

1)できればConfigureAwait(false)を使ってください。

たとえば、await MyAsync().ConfigureAwait(false);ではなくawait MyAsync();です。

ConfigureAwait(false)は、現在のコンテキストで再開する必要がないことをawaitに伝えます(この場合、「現在のコンテキストで」とは「UIスレッドで」という意味です)。ただし、残りのasyncメソッド(ConfigureAwaitの後)については、現在のコンテキストにいることを前提とした操作(UI要素の更新など)はできません。

詳細については、MSDNの記事 非同期プログラミングのベストプラクティス )を参照してください。

2)Task.Runを使用してCPUバインドメソッドを呼び出します。

Task.Runを使用する必要がありますが、再利用可能にしたいコード(つまりライブラリコード)内では使用しないでください。そのため、メソッドの実装の一部としてではなく、メソッドのcallTask.Runを使用します。

そのため、純粋にCPUバウンドの作業は次のようになります。

// Documentation: This method is CPU-bound.
void DoWork();

あなたはTask.Runを使ってそれを呼ぶでしょう:

await Task.Run(() => DoWork());

CPUバウンドとI/OバウンドのmixedであるメソッドはそれらのCPUバウンドの性質を指摘するドキュメンテーションと共にAsyncシグネチャを持つべきです:

// Documentation: This method is CPU-bound.
Task DoWorkAsync();

これはTask.Runを使用して呼び出すこともできます(これは部分的にCPUに依存しているためです)。

await Task.Run(() => DoWorkAsync());
309
Stephen Cleary

あなたのContentLoaderに関する1つの問題は、内部的にそれが順次動作するということです。より良いパターンは、作業を並列化し、最後に同期化することです。

public class PageViewModel : IHandle<SomeMessage>
{
   ...

   public async void Handle(SomeMessage message)
   {
      ShowLoadingAnimation();

      // makes UI very laggy, but still not dead
      await this.contentLoader.LoadContentAsync(); 

      HideLoadingAnimation();   
   }
}

public class ContentLoader 
{
    public async Task LoadContentAsync()
    {
        var tasks = new List<Task>();
        tasks.Add(DoCpuBoundWorkAsync());
        tasks.Add(DoIoBoundWorkAsync());
        tasks.Add(DoCpuBoundWorkAsync());
        tasks.Add(DoSomeOtherWorkAsync());

        await Task.WhenAll(tasks).ConfigureAwait(false);
    }
}

明らかに、これは他の以前のタスクからのデータを必要とするタスクがある場合はうまくいきませんが、ほとんどのシナリオで全体的なスループットが向上するはずです。

9
Paul Hatcher