我想问您关于何时使用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的最佳位置在哪里?

我应该


包装外部调用,因为这对于.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)中的行等待Task.Run(async()=>等待this.contentLoader.LoadContentAsync());应该只是等待Task.Run(()=> this.contentLoader.LoadContentAsync());。 AFAIK通过在Task.Run中添加第二次等待和异步不会获得任何收益。而且由于您没有传递参数,因此稍稍简化了等待Task.Run(this.contentLoader.LoadContentAsync);.

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

如果只有一系列同步方法,则可以使用等待模式Task.Run(()=> {RunAnySynchronousMethod(); return Task.CompletedTask;});在您的异步方法(例如异步控制器方法,测试方法等)中。

#1 楼

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

一次阻塞UI线程的时间不要超过50毫秒。
您可以安排每秒〜100个UI线程连续性; 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());


评论


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

–卢卡斯K
13年8月2日在14:16

您的所有库方法都应使用ConfigureAwait(false)。如果先执行此操作,则可能会发现Task.Run完全不必要。如果您仍然需要Task.Run,​​那么无论您调用一次还是多次,它都不会对运行时造成太大影响,因此只需执行最自然的代码即可。

–斯蒂芬·克莱里(Stephen Cleary)
2013年8月2日14:21



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

–德雷克
2015年10月5日19:46

@ user4205580:否,Task.Run可以理解异步签名,因此在DoWorkAsync完成之前它不会完成。不需要额外的异步/等待。我将在Task.Run礼节的博客系列中解释更多“为什么”的信息。

–斯蒂芬·克莱里(Stephen Cleary)
15年11月14日在16:17

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

–斯蒂芬·克莱里(Stephen Cleary)
15年11月14日在21:08

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


显然,如果任何任务都需要来自其他早期任务,但在大多数情况下应该可以为您提供更好的总体吞吐量。