ChatGPT解决这个技术问题 Extra ChatGPT

正确使用 Task.Run 和 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();
    }
}

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

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

我应该只是

包装外部调用,因为这对 .NET 的线程工作较少,或者我应该只包装内部运行的 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 通过在 Task.Run 中添加第二个 awaitasync 不会获得任何收益。而且由于您没有传递参数,因此对 await Task.Run( this.contentLoader.LoadContentAsync ); 的简化程度更高。
如果您在里面有第二个等待,实际上会有一点不同。请参阅此article。我发现它非常有用,只是在这一点上我不同意并且更喜欢直接返回 Task 而不是等待。 (正如您在评论中建议的那样)
如果您只有一系列同步方法,您可以在异步方法中使用模式 await Task.Run(() => { RunAnySynchronousMethod(); return Task.CompletedTask; });(例如异步控制器方法、测试方法等)。

S
Stephen Cleary

请注意收集在我的博客上的 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<T> 或其简写符号之一,例如 FromAsync。我有一个 blog post that goes into more detail why async methods don't require threads
P
Paul Hatcher

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 线程上恢复。还是我完全误解了这是如何工作的?