使用 Task.Run 与 Async/Await 的高级技巧
加深理解
在本系列的上一篇指南中,我们了解了为什么Task.Run主要用于 CPU 密集型代码。在探索该主题时,很明显,尽管您可以将Task.Run与其他类型的操作一起使用,但这可能不是系统资源的最佳利用方式。我们还看到了等待对Task.Run的调用是多么容易。但这当然不是全部。如果您知道一些技巧,它们可以为您省去很多麻烦。
在哪里调用 Task.Run
让我们从上一个指南中应用程序的结尾继续。该应用程序下载了一个图像,然后使用名为 ImageSharp 的库(在 NuGet 中以SixLabors.ImageSharp的形式提供)对其进行模糊处理。我们定义了一个名为BlurImage的方法,如下所示:
static async Task<byte[]> BlurImage(string imagePath)
{
return await Task.Run(() =>
{
var image = Image.Load(imagePath);
image.Mutate(ctx => ctx.GaussianBlur());
using (var memoryStream = new MemoryStream())
{
image.SaveAsJpeg(memoryStream);
return memoryStream.ToArray();
}
});
}
请注意,对Task.Run的调用紧接在图像处理代码之前。这是最好的方法吗?
无论是为了方便还是为了清晰,您可能会发现自己将对Task.Run 的调用放在尽可能靠近 CPU 密集型代码的位置,就像上面的方法一样。但是,随着应用程序的复杂性增加,这种方法就变得不是最佳选择。为了说明这一点,想象一下,如果将来我们想在应用程序中添加一个可以旋转、变暗和模糊的方法。我们可以从编写以下内容开始:
static async Task ProcessImage(byte[] imageData)
{
await Task.Run(() =>
{
RotateImage(imageData);
DarkenImage(imageData);
BlurImage(imageData);
}
}
但是我们注意到BlurImage(或接受字节数组的版本)已经返回了一个Task,因此我们将其更改为:
await Task.Run(async () =>
{
RotateImage(imageData);
DarkenImage(imageData);
await BlurImage(imageData);
}
然后我们注意到BlurImage本身调用了Task.Run,这意味着我们现在有一个嵌套的 Task.Run调用。所以我们会在另一个线程中启动一个线程。这也不是对系统资源的最佳利用,而且可能会对性能产生负面影响。这就是为什么不鼓励库作者在库方法中使用Task.Run的原因:何时启动线程应该由调用者决定。
因此,通常建议将对Task.Run的调用尽可能靠近 UI 代码和事件处理程序。按照该建议,您会发现大多数 CPU 密集型代码最终都会被编写为同步代码,而Task.Run会放在最外层的调用方法中。因此,在此示例中,我们最终会得到类似以下内容的结果:
static async void OnButtonClick()
{
byte[] imageData = await LoadImage();
await Task.Run(() => ProcessImage(ref imageData));
await SaveImage(imageData);
}
static void ProcessImage(ref byte[] imageData)
{
RotateImage(ref imageData);
DarkenImage(ref imageData);
BlurImage(ref imageData);
}
...而BlurImage将会是:
static void BlurImage(ref byte[] imageData)
{
var image = Image.Load(imageData);
image.Mutate(ctx => ctx.GaussianBlur());
using (var memoryStream = new MemoryStream())
{
image.SaveAsJpeg(memoryStream);
imageData = memoryStream.ToArray();
}
}
没有必要继续主线程
您可能还记得,await与Task.Run一起使用时会捕获有关当前线程的信息。这样做是为了在另一个线程上完成处理后,可以在原始线程上继续执行。但如果调用方法中的其余代码不需要在原始线程上运行怎么办?
在这种情况下,你可以通过告诉await你不想继续在原始上下文中运行来提高代码的性能。这是通过使用名为ConfigureAwait的Task方法完成的。一个很好的例子是我们之前定义的OnButtonClick方法:
static async void OnButtonClick()
{
byte[] imageData = await LoadImage();
Task processImageTask = Task.Run(() => ProcessImage(ref imageData));
await processImageTask.ConfigureAwait(false);
await SaveImage(imageData);
}
不过,为此定义一个变量有点冗长,所以大多数情况下只需将其附加到对Task.Run的调用的末尾:
static async void OnButtonClick()
{
byte[] imageData = await LoadImage();
await Task.Run(() => ProcessImage(ref imageData)).ConfigureAwait(false);
await SaveImage(imageData);
}
ConfigureAwait的参数是一个名为continueOnCapturedContext的布尔值,默认值为true。通过传递false,我们表示我们希望在线程池线程而不是 UI 线程上继续执行该方法的其余部分。只要您没有在 await 之后的代码中修改任何 UI 元素(或在那里执行任何其他需要应用程序主线程的操作),您就可以安全地使用此技术来启用一定程度的并行性。
特别鼓励使用await的库作者使用ConfigureAwait(false),因为不这样做可能会导致死锁,具体取决于应用程序开发人员使用库方法的方式。大多数时候,库代码并不关心它在哪个线程上运行,因此使用ConfigureAwait(false)将确保您的库代码永远不会在主线程上等待。
现在我必须承认,ConfigureAwait(false)并不是最好的语法,它的存在确实会使代码有些混乱。事实上,我希望有更好的方法。但是,在应用程序的主线程上运行的越少,应用程序在最终用户看来就越快。因此,请在适用的情况下使用ConfigureAwait(false)。您的应用程序的用户会感谢您!
我应该将 Task.Run 与 ASP.NET Core 一起使用吗?
到目前为止,我们已经讨论了基于 UI 的应用程序,但是有关Task.Run的信息是否适用于 ASP.NET Core 等 Web 应用程序框架?
在 ASP.NET Core 中使用async/await确实有很多优点,但Task.Run却并非如此。事实证明,在服务网页时使用线程池线程没有多大意义。通常,在 ASP.NET 中,每个请求都有一个线程,并且您希望能够同时处理尽可能多的请求。在这种情况下使用Task.Run实际上会降低可扩展性,因为您减少了可用于处理新请求的线程数。此外,使用单独的线程不会对响应性产生任何影响,因为用户无论如何都无法与页面交互,直到页面加载完成。一旦页面加载完成,响应性主要由用户的客户端浏览器交互(以及 JavaScript 代码的质量)决定,而不是由 ASP.NET 决定。因此,对于 ASP.NET 中 CPU 绑定的代码,最好坚持同步处理。简而言之,避免在 ASP.NET 应用程序中使用Task.Run。如果您使用async/await,请专注于自然异步的 I/O 操作!
那么 Task.Factory.StartNew 怎么样?
您可能会在Task.Factory.StartNew中偶然发现类似的方法,并对它感到好奇,因此值得简要提及。实际上,Task.Factory.StartNew中的方法是在Task.Run之前引入的,并且更易于配置。但是,最好坚持使用Task.Run。除了一些非常特殊的需求,这些需求远远超出了正常的应用程序要求(更不用说本指南的范围),您实际上永远不需要Task.Factory.StartNew提供的额外复杂性,而且Task.Run无论如何都更简洁。不要试图耍小聪明;只需使用Task.Run!
结论
对于希望保持应用程序响应速度的任何 C# 开发人员来说,了解何时以及如何使用Task.Run非常重要。正如我们在 ASP.NET 中看到的那样,有时答案是根本不使用Task.Run!相比之下,对于具有用户界面的应用程序,它是以非阻塞方式运行 CPU 绑定代码的主要方式。对于这些情况,请记住有关将Task.Run 调用放在何处的最佳实践,您一定会发现将它与async/await结合使用会取得成功。
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~