正确使用 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 () =&gt; ... ) 包装它。使用 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 () =&gt; await this.contentLoader.LoadContentAsync()); 中的行应该只是 await Task.Run( () =&gt; this.contentLoader.LoadContentAsync() );。 AFAIK 在Task.Run 中添加第二个awaitasync 不会获得任何收益。而且由于您没有传递参数,因此简化为await Task.Run( this.contentLoader.LoadContentAsync ); 如果你有第二个 await 里面实际上有一点区别。请参阅此article。我发现它非常有用,只是在这一点上我不同意并且更喜欢直接返回 Task 而不是等待。 (正如您在评论中建议的那样) 如果您只有一系列同步方法,您可以在异步方法(例如异步控制器方法、测试方法等)中使用模式await Task.Run(() =&gt; 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&lt;T&gt; 或其简写符号之一,例如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.Run 和 Task.Factory.StartNew 区别

关于 Task.Start() 、 Task.Run() 和 Task.Factory.StartNew() 的使用

Task.Run 和Task.Factory.StartNew 区别

Parallel.ForEach 与 Task.Run 和 Task.WhenAll