我想询问您对何时使用 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
不一定在后台线程上运行,要在后台开始工作,您需要用 await Task.Run(async () => ... )
包装它。使用 async
await
不会阻塞 UI,但它仍然在 UI 线程上运行,因此它会变得滞后。
放置 Task.Run 的最佳位置在哪里?
我应该只是
包装外部调用,因为这对 .NET 的线程工作较少,或者我应该只包装内部运行的 CPU 绑定方法 Task.Run ,因为这使它可以在其他地方重用?我不确定在核心深处的后台线程上开始工作是否是一个好主意。
广告(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.
广告(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
}
await Task.Run(async () => await this.contentLoader.LoadContentAsync());
中的行应该只是 await Task.Run( () => this.contentLoader.LoadContentAsync() );
。 AFAIK 通过在 Task.Run
中添加第二个 await
和 async
不会获得任何收益。而且由于您没有传递参数,因此对 await Task.Run( this.contentLoader.LoadContentAsync );
的简化程度更高。
await Task.Run(() => { RunAnySynchronousMethod(); return Task.CompletedTask; });
(例如异步控制器方法、测试方法等)。
请注意收集在我的博客上的 guidelines for performing work on a UI thread:
一次阻塞 UI 线程的时间不要超过 50 毫秒。
您可以每秒在 UI 线程上安排约 100 个延续; 1000太多了。
您应该使用两种技术:
1) 尽可能使用 ConfigureAwait(false)
。
例如,await MyAsync().ConfigureAwait(false);
而不是 await MyAsync();
。
ConfigureAwait(false)
告诉 await
您不需要在当前上下文中恢复(在这种情况下,“在当前上下文中”是指“在 UI 线程上”)。但是,对于该 async
方法的其余部分(在 ConfigureAwait
之后),您不能做任何假设您在当前上下文中的事情(例如,更新 UI 元素)。
有关详细信息,请参阅我的 MSDN 文章 Best Practices in Asynchronous Programming。
2) 使用 Task.Run
调用受 CPU 限制的方法。
您应该使用 Task.Run
,但不要在您希望可重用的任何代码(即库代码)中使用。因此,您使用 Task.Run
来调用方法,而不是作为方法的实现的一部分。
所以纯粹受 CPU 限制的工作看起来像这样:
// Documentation: This method is CPU-bound.
void DoWork();
您将使用 Task.Run
调用它:
await Task.Run(() => DoWork());
受 CPU 限制和受 I/O 限制混合的方法应具有 Async
签名,并带有指出其受 CPU 限制性质的文档:
// Documentation: This method is CPU-bound.
Task DoWorkAsync();
您还可以使用 Task.Run
调用它(因为它部分受 CPU 限制):
await Task.Run(() => DoWorkAsync());
ContentLoader 的一个问题是它在内部按顺序运行。更好的模式是将工作并行化,然后在最后进行同步,所以我们得到
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);
}
}
显然,如果任何任务需要来自其他早期任务的数据,这将不起作用,但在大多数情况下应该为您提供更好的整体吞吐量。
ConfigureAwait(false)
。如果您先这样做,那么您可能会发现Task.Run
完全没有必要。如果您确实仍然需要Task.Run
,那么在这种情况下,无论您调用一次还是多次调用它都不会对运行时产生太大影响,因此只需对您的代码执行最自然的操作即可。ConfigureAwait(false)
,它仍然是 UI 线程将执行 cpu-bound 方法,并且只有之后的所有内容可能在 TP 线程上完成。还是我误会了什么?Task.Run
理解异步签名,因此在DoWorkAsync
完成之前它不会完成。额外的async
/await
是不必要的。我在 blog series onTask.Run
etiquette 中解释了更多“为什么”。TaskCompletionSource<T>
或其简写符号之一,例如FromAsync
。我有一个 blog post that goes into more detail why async methods don't require threads。