何时正确使用Task.Run以及何时仅异步等待


317

我想问您关于何时使用正确的体系结构的意见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的最佳位置在哪里?

我应该

  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
}

顺便说一句,(1)中的行await Task.Run(async () => await this.contentLoader.LoadContentAsync());应该只是await Task.Run( () => this.contentLoader.LoadContentAsync() );。AFAIK通过在里面加一秒await并没有任何收获。而且由于您没有传递参数,因此可以简化到。asyncTask.Runawait Task.Run( this.contentLoader.LoadContentAsync );
制造商史蒂夫(Steve)

如果您有第二次等待,实际上会有一点差异。看到这篇文章。我发现它非常有用,只是在这一点上我并不同意,宁愿直接返回Task而不是等待。(如您在评论中所建议)
Lukas K,

Answers:


365

请注意在我的博客上收集的有关在UI线程上执行工作准则

  • 一次不要将UI线程阻塞超过50毫秒。
  • 您可以每秒在UI线程上安排约100个连续时间;1000太多了。

您应该使用两种技术:

1)ConfigureAwait(false)可以的话使用。

例如,await MyAsync().ConfigureAwait(false);而不是await MyAsync();

ConfigureAwait(false)告诉await您不需要在当前上下文上恢复(在这种情况下,“在当前上下文上”表示“在UI线程上”)。但是,对于该async方法的其余部分(ConfigureAwait),您不能做任何假设您处于当前上下文中的操作(例如,更新UI元素)。

有关更多信息,请参见我的MSDN文章异步编程最佳实践

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());

4
感谢您的快速回复!我知道您发布的链接,并看到了博客中引用的视频。实际上,这就是为什么我发布此问题的原因-在视频中说(与您的回复相同),您不应在核心代码中使用Task.Run。但是我的问题是,我每次使用该方法时都需要包装这种方法,以免减慢响应速度(请注意,我所有的代码都是异步的并且没有阻塞,但是没有Thread.Run则太慢了)。我是否只是将CPU绑定的方法(许多Task.Run调用)包装或将所有内容完全包装在一个Task.Run中是更好的方法?
Lukas K

12
您的所有库方法都应使用ConfigureAwait(false)。如果您首先这样做,那么您可能会发现Task.Run完全没有必要。如果你还是需要Task.Run的话,并没有太大的区别,以运行在这种情况下,你是否叫过一次或多次,所以才做什么是最自然的,你的代码。
Stephen Cleary

2
我不了解第一种技术将如何帮助他。即使您使用ConfigureAwait(false)cpu绑定方法,仍然是UI线程将执行cpu绑定方法,并且只有之后的所有事情都可以在TP线程上完成。还是我误会了什么?
Darius 2015年

4
@ user4205580:否,Task.Run了解异步签名,因此直到完成才DoWorkAsync完成。多余的async/ await是不必要的。我将在有关Task.Run礼节的博客系列中解释更多“为什么”的信息。
Stephen Cleary 2015年

3
@ user4205580:否。绝大多数“核心”异步方法不在内部使用它。在正常实施的“核心”异步方法的方式是使用TaskCompletionSource<T>或其速记符号之一如FromAsync。我有一篇博客文章,其中详细介绍了为什么异步方法不需要线程
Stephen Cleary

12

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

显然,如果任何任务都需要其他早期任务中的数据,则此方法不起作用,但在大多数情况下应为您提供更好的总体吞吐量。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.