何时正确使用Task.Run,何时只需使用async-await?

432

我想请你就何时使用Task.Run的正确架构发表意见。我们的WPF .NET 4.5应用程序(使用Caliburn Micro框架)界面响应迟缓。

基本上,我的代码(非常简化的代码段)如下:

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不一定在后台线程上运行,如果要在后台工作,则需要使用await Task.Run(async () => ... )来包装它。使用asyncawait不会阻塞UI,但仍在UI线程上运行,因此会使其卡顿。
最佳放置Task.Run的位置是哪里?
我应该只是
  1. 包装外部调用,因为这对.NET来说是较少的线程工作,还是
  2. 仅在内部包装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
}

2
顺便提一下,(1)中的这行代码await Task.Run(async () => await this.contentLoader.LoadContentAsync());可以简化为await Task.Run(() => this.contentLoader.LoadContentAsync());。据我所知,在Task.Run内部添加第二个awaitasync不会带来任何收益。由于您没有传递参数,因此可以进一步简化为await Task.Run(this.contentLoader.LoadContentAsync); - ToolmakerSteve
如果你有第二个 await 的话,实际上会有一点点不同。可以查看这篇 文章。我发现它非常有用,只是在这个特定的点上,我不同意并且更喜欢直接返回 Task 而不是等待(就像你在评论中建议的那样)。 - Lukas K
如果您只有一系列同步方法,您可以在异步方法(例如异步控制器方法、测试方法等)中使用模式 await Task.Run(() => { RunAnySynchronousMethod(); return Task.CompletedTask; }); - Matt
相关:await Task.Run vs await - Theodor Zoulias
2个回答

483

请注意在我的博客中收集的针对 UI 线程执行工作的指南:guidelines for performing work on a UI thread

  • 不要一次阻塞 UI 线程超过 50 毫秒。
  • 您可以每秒在 UI 线程上调度 ~100 个 continuation;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-bound和I/O-bound方法应该具有带有文档说明其CPU-bound特性的 Async 签名:

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

您也可以使用Task.Run来调用它(因为它部分受CPU限制):

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

5
谢谢您的快速回复!我知道您发的链接并且看了您博客中提到的视频。实际上这就是我发这个问题的原因——视频中说(与您的回答相同)在核心代码中不应该使用Task.Run。但我的问题是,每次使用此类方法时都需要将其包装起来以避免降低响应速度(请注意,我所有的代码都是异步的,并且没有阻塞,但没有Thread.Run它就会变得很卡)。我也很困惑,是更好地仅包装CPU绑定的方法(多个Task.Run调用),还是完全将所有东西都包装在一个Task.Run中? - Lukas K
17
你的所有库方法都应该使用ConfigureAwait(false)。如果你先这样做,你可能会发现Task.Run完全不必要。如果你确实还需要Task.Run,那么在这种情况下,调用一次或多次对运行时来说并没有太大的区别,所以只需按照代码最自然的方式处理即可。 - Stephen Cleary
4
我不理解第一种技术如何帮助他。即使在CPU密集型方法上使用ConfigureAwait(false),UI线程仍将执行CPU密集型方法,只有之后的内容可能会在TP线程上执行。或者我误解了什么? - Drake
7
@user4205580: 不需要额外的async/await,因为Task.Run可以理解异步签名,所以它将在DoWorkAsync完成之前不会结束。我在我的 关于Task.Run规范的博客系列中更详细地解释了其中的原因。 - Stephen Cleary
3
不。绝大多数“核心”的异步方法在内部不使用它。实现“核心”异步方法的正常方式是使用TaskCompletionSource<T>或其缩写符号之一,例如FromAsync。我有一篇博客文章详细介绍了为什么异步方法不需要线程 - Stephen Cleary
显示剩余27条评论

22

你的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);
    }
}

显然,如果任何任务需要来自其他较早任务的数据,则此方法无效,但对于大多数情况,这应该能够提高总吞吐量。


2
这里不应该用ConfigureAwait(false)吗?实际上,您确实希望在LoadContentAsync方法完成后恢复到UI线程。还是我完全误解了它的工作原理? - Achilles P.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接