正确使用Task.Run时和async-await时

Luk*_*s K 288 c# asynchronous task async-await

我想问你关于正确架构何时使用的意见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();
    }
}
Run Code Online (Sandbox Code Playgroud)

从我阅读/看到的文章/视频中,我知道await async不一定在后台线程上运行,并且需要在后台开始工作,需要等待它Task.Run(async () => ... ).使用async await不会阻止UI,但它仍然在UI线程上运行,因此它使它变得迟钝.

放置Task.Run的最佳位置在哪里?

我应该

  1. 包装外部调用因为这不是.NET的线程工作

  2. ,或者我应该只包装内部运行的CPU绑定方法,Task.Run因为这使得它可以重用于其他地方?我不确定这里是否开始深入核心的后台线程是一个好主意.

Ad(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.
Run Code Online (Sandbox Code Playgroud)

广告(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
}
Run Code Online (Sandbox Code Playgroud)

Ste*_*ary 338

请注意在我的博客上收集的在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();
Run Code Online (Sandbox Code Playgroud)

你打电话给谁Task.Run:

await Task.Run(() => DoWork());
Run Code Online (Sandbox Code Playgroud)

CPU绑定和I/O绑定混合的方法应该有一个Async签名,文档指出它们的CPU绑定性质:

// Documentation: This method is CPU-bound.
Task DoWorkAsync();
Run Code Online (Sandbox Code Playgroud)

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

await Task.Run(() => DoWorkAsync());
Run Code Online (Sandbox Code Playgroud)

  • 你的所有库方法都应该使用`ConfigureAwait(false)`.如果你先这样做,那么你可能会发现`Task.Run`是完全没必要的.如果你*仍然需要`Task.Run`,那么在这种情况下它对运行时没有多大影响,无论你是一次还是多次调用它,所以只需对你的代码做最自然的事情. (12认同)
  • 谢谢你的快速响应!我知道您发布的链接,并看到了您博客中引用的视频.实际上这就是我发布这个问题的原因 - 在视频中说(与你的回复相同)你不应该在核心代码中使用Task.Run.但我的问题是,我每次使用它时都需要包装这样的方法来减慢响应速度(请注意我的所有代码都是异步的并且没有阻塞,但没有Thread.Run它只是滞后).我很困惑是否更好的方法来包装CPU绑定方法(许多Task.Run调用)或completly包装在一个Task.Run中的所有东西? (4认同)
  • @ user4205580:不,`Task.Run`理解异步签名,所以在'DoWorkAsync`完成之前它不会完成.额外的`async` /`await`是不必要的.我在[Task.Run`礼仪博客系列](http://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-even-in.html)中解释了更多关于"为什么"的内容. (4认同)
  • @ user4205580:不会.绝大多数"核心"异步方法都不在内部使用它.实现"核心"异步方法的*normal*方法是使用`TaskCompletionSource <T>`或其简写符号之一,例如`FromAsync`.我有一篇[博文更详细地说明为什么异步方法不需要线程](http://blog.stephencleary.com/2013/11/there-is-no-thread.html). (3认同)
  • 我不明白第一种技术将如何帮助他.即使你在你的cpu绑定方法上使用`ConfigureAwait(false)`,它仍然是要执行cpu绑定方法的UI线程,并且只有在TP线程之后才能完成所有操作.还是我误解了什么? (2认同)
  • @Darius:我推荐两种技术一起使用.这不是两个答案中的一个; 这是两个部分的答案. (2认同)
  • @user4205580:是的,额外的括号会使 lambda 返回 `void` 而不是 `Task`/`Task&lt;T&gt;`。所以`() =&gt; { return DoWorkAsync(); }` 也应该工作。 (2认同)
  • 谢谢。我读过你的几篇文章,也是一篇试图解释为什么在实现层面使用“Task.Run”不好的文章。我会说有时`Task.Run` 是必要的(当然这里的重点是在该调用中包装尽可能少的行,而不是我的异步方法的整个主体)。如果我的异步方法不使用现有的异步方法,那么除了在其代码中使用 `await Task.Run` 使其真正异步之外别无他法,对吗?我猜标准 C# 异步方法使用它。 (2认同)
  • @VISHMAY:我建议您问自己的SO问题。很难在SO注释中编写/阅读代码。 (2认同)
  • @EugenePodskal:可以用任何一种方式编写。[省略 `async` 和 `await`](https://blog.stephencleary.com/2016/12/eliding-async-await.html) 当它只是像这样的简单传递时很常见。 (2认同)

Pau*_*her 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

显然,如果任何任务需要来自其他早期任务的数据,这不起作用,但应该为大多数情况提供更好的整体吞吐量.

  • 这里的 .ConfigureAwait(false) 是不是不正确?实际上,您确实希望在 LoadContentAsync 方法完成后恢复 UI 线程。或者我完全误解了它是如何工作的? (2认同)