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();
}
}
私が読んだり見た記事やビデオから、await
async
は必ずしもバックグラウンドスレッドで実行されているわけではなく、バックグラウンドで作業を始めるためにはTask.Run(async () => ... )
でラップする必要があります。 async
await
を使用してもUIはブロックされませんが、まだUIスレッド上で実行されているため、遅れています。
Task.Runを置くのに最適な場所はどこですか?
私だけ
これは.NETのスレッド化作業が少ないため、外部呼び出しをラップします。
それとも他の場所で再利用可能になるので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
}
私のブログで収集した UIスレッド で作業を実行するためのガイドライン:)に注意してください。
使用すべき2つの手法があります。
1)できればConfigureAwait(false)
を使ってください。
たとえば、await MyAsync().ConfigureAwait(false);
ではなくawait MyAsync();
です。
ConfigureAwait(false)
は、現在のコンテキストで再開する必要がないことをawait
に伝えます(この場合、「現在のコンテキストで」とは「UIスレッドで」という意味です)。ただし、残りのasync
メソッド(ConfigureAwait
の後)については、現在のコンテキストにいることを前提とした操作(UI要素の更新など)はできません。
詳細については、MSDNの記事 非同期プログラミングのベストプラクティス )を参照してください。
2)Task.Run
を使用してCPUバインドメソッドを呼び出します。
Task.Run
を使用する必要がありますが、再利用可能にしたいコード(つまりライブラリコード)内では使用しないでください。そのため、メソッドの実装の一部としてではなく、メソッドのcallにTask.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());
あなたの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);
}
}
明らかに、これは他の以前のタスクからのデータを必要とするタスクがある場合はうまくいきませんが、ほとんどのシナリオで全体的なスループットが向上するはずです。