正确使用 Task.Run 和 async-await 时
Posted
技术标签:
【中文标题】正确使用 Task.Run 和 async-await 时【英文标题】:When correctly use Task.Run and when just async-await 【发布时间】:2022-01-07 02:41:54 【问题描述】:我想询问您对何时使用Task.Run
的正确架构的看法。我在我们的 WPF .NET 4.5 中遇到了滞后的 UI
应用程序(使用 Caliburn Micro 框架)。
基本上我在做(非常简化的代码sn-ps):
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 的线程工作较少
,或者我应该只包装在内部使用Task.Run
运行的受 CPU 限制的方法,因为这使它可以在其他地方重用?我不确定在核心深处的后台线程上开始工作是否是一个好主意。
广告(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
【问题讨论】:
顺便说一句,(1)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 里面实际上有一点区别。请参阅此article。我发现它非常有用,只是在这一点上我不同意并且更喜欢直接返回 Task 而不是等待。 (正如您在评论中建议的那样)
如果您只有一系列同步方法,您可以在异步方法(例如异步控制器方法、测试方法等)中使用模式await Task.Run(() => RunAnySynchronousMethod(); return Task.CompletedTask; );
。
相关:await Task.Run vs await
【参考方案1】:
注意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());
【讨论】:
感谢您的快速回复!我知道您发布的链接,并看到了您博客中引用的视频。实际上这就是我发布这个问题的原因 - 在视频中说(与您的回复相同)您不应该在核心代码中使用 Task.Run 。但我的问题是,每次我使用它时都需要包装这样的方法,以免减慢响应速度(请注意我的所有代码都是异步的并且没有阻塞,但是没有 Thread.Run 它只是滞后)。我也很困惑,将 CPU 绑定方法(许多 Task.Run 调用)包装起来还是将所有内容完全包装在一个 Task.Run 中是更好的方法? 你所有的库方法都应该使用ConfigureAwait(false)
。如果您先这样做,那么您可能会发现 Task.Run
完全没有必要。如果您确实仍然需要Task.Run
,那么在这种情况下,无论您调用一次还是多次调用它都不会对运行时产生太大影响,所以只需为您的代码做最自然的事情。
我不明白第一种技术对他有什么帮助。即使您在 cpu-bound 方法上使用 ConfigureAwait(false)
,它仍然是 UI 线程将执行 cpu-bound 方法,并且只有之后的所有事情都可能在 TP 线程上完成。还是我误会了什么?
@user4205580: 不,Task.Run
理解异步签名,所以在DoWorkAsync
完成之前它不会完成。额外的async
/await
是不必要的。我在blog series on Task.Run
etiquette 中解释了更多“为什么”。
@user4205580:不。绝大多数“核心”异步方法在内部不使用它。实现“核心”异步方法的正常方法是使用TaskCompletionSource<T>
或其简写符号之一,例如FromAsync
。我有一个blog post that goes into more detail why async methods don't require threads。【参考方案2】:
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) 不正确吗?在 LoadContentAsync 方法完成后,您实际上确实希望在 UI 线程上恢复。还是我完全误解了它的工作原理?以上是关于正确使用 Task.Run 和 async-await 时的主要内容,如果未能解决你的问题,请参考以下文章
使用异步方法等待 Task.Run 不会在正确的线程上引发异常
Task.Run 和 Task.Factory.StartNew 区别
关于 Task.Start() 、 Task.Run() 和 Task.Factory.StartNew() 的使用