开始使用 C# 的 Async 和 Await 关键字
异步性的需求
许多应用程序都具有这样的功能:出于这样或那样的原因,它需要等待某件事完成。这可能是一个过程、计算、Web 请求或其他一些输入/输出操作。与同步等待此类操作完成(这可能效率低下和/或导致应用程序冻结)不同,通常需要异步执行此类等待,以便应用程序保持响应并能够在等待期间执行其他操作。在 C# 中,使用async和await是进行此类异步等待的主要方式,本指南将帮助您开始使用这种方法。但正如本系列上一篇指南中提到的,可以在 C# 中进行异步编程,而无需这些关键字。那么为什么要引入这些关键字?它们为什么如此重要?
不使用 Async 和 Await 的异步编程
为了充分理解async / await方法提供的功能,让我们首先编写一些不使用该方法的异步代码。假设我们想要一个简单的控制台应用程序来下载和模糊图像,并且我们希望异步执行下载和模糊。通过维护一个带有当前时间(以毫秒为单位)的“仪表板”,我们可以证明应用程序在整个执行过程中保持响应。
static bool done = false;
static void Main()
{
DownloadAndBlur();
while (!done)
{
Console.CursorLeft = 0;
Console.Write(DateTime.Now.ToString("HH:mm:ss.fff"));
Thread.Sleep(50);
}
Console.WriteLine();
Console.WriteLine("Done!");
}
现在,让我们解决下载和模糊问题。.NET 任务并行库通过称为 Task 的抽象提供对异步等待的支持,无论您需要执行哪种类型的操作都可以使用它。如果我们构建代码以利用Task,我们可能为每个操作都有一个 C# 方法。
static Task<byte[]> DownloadImage(string url) { ... }
static Task<byte[]> BlurImage(string imagePath) { ... }
static Task SaveImage(byte[] bytes, string imagePath) { ... }
最后,我们需要定义DownloadAndBlur,它将使用 .NET 任务并行库调用上述方法。
static void DownloadAndBlur()
{
var url = "https://...jpg";
var fileName = Path.GetFileName(url);
DownloadImage(url).ContinueWith(task1 =>
{
var originalImageBytes = task1.Result;
var originalImagePath = Path.Combine(ImageResourcesPath, fileName);
SaveImage(originalImageBytes, originalImagePath).ContinueWith(task2 =>
{
BlurImage(originalImagePath).ContinueWith(task3 =>
{
var blurredImageBytes = task3.Result;
var blurredFileName = $"{Path.GetFileNameWithoutExtension(fileName)}_blurred.jpg";
var blurredImagePath = Path.Combine(ImageResourcesPath, blurredFileName);
SaveImage(blurredImageBytes, blurredImagePath).ContinueWith(task4 =>
{
done = true;
});
});
});
});
}
由于我们使用了Task,因此每个方法的调用都会立即返回,在它对应的“任务”(即操作)完成之前。同样,ContinueWith定义了一个额外的任务,该任务对应于上一个任务完成时应该发生的事情。因此,DownloadAndBlur方法的执行非常快地完成,并且“仪表板”在您开始运行应用程序后立即开始更新。简而言之,一切都以非阻塞、异步的方式完成,这一切都要归功于 .NET 任务并行库。
但是您可能会注意到,由于使用回调, DownloadAndBlur中的代码有点难以理解。对于每个任务,我们必须添加一定级别的缩进,以及伴随任何方法的括号和花括号。这种方法的一个不幸的副作用是可读性降低。更重要的是,乍一看很难辨别出这段代码是异步的。显然,阅读代码不仅要理解单词;还要理解代码在运行时执行时的流程。幸运的是,C# 开发人员确实有一种方法可以改善这两个方面。
使用 Async 和 Await 进行异步编程
让我们重写DownloadAndBlur方法,但这次使用 async和await。
static async void DownloadAndBlur()
{
var url = "https://...jpg";
var fileName = Path.GetFileName(url);
var originalImageBytes = await DownloadImage(url);
var originalImagePath = Path.Combine(ImageResourcesPath, fileName);
await SaveImage(originalImageBytes, originalImagePath);
var blurredImageBytes = await BlurImage(originalImagePath);
var blurredFileName = $"{Path.GetFileNameWithoutExtension(fileName)}_blurred.jpg";
var blurredImagePath = Path.Combine(ImageResourcesPath, blurredFileName);
await SaveImage(blurredImageBytes, blurredImagePath);
done = true;
}
请注意,不再有任何回调,因此不再需要相应的缩进、括号和圆括号。也不再有对ContinueWith的引用,从而使代码更易于阅读和理解。事实上,我们对DownloadAndBlur方法所做的大部分更改都是删除代码。主要添加的是在每个返回Task 的方法前面添加一个await关键字。因此,使用async / await不仅更易读,而且也更容易编写!
您可能已经想到,await会导致执行以非阻塞方式暂停,直到相应操作完成,然后继续执行后续代码。需要注意的是,您希望 await 的方法必须返回Task。(从这个意义上讲, C# 中的Task类似于现代 JavaScript 中的Promise。)如果您尝试 await 不返回Task的东西,您的代码将无法编译。鉴于await关键字相当简单,在 C# 中使代码异步的主要挑战实际上是确保您的代码结构化以使用Task。一旦您做到这一点,添加await就不那么困难了。
但是async关键字呢?我们将其添加到DownloadAndBlur方法签名的static和void之间,但这真的有必要吗?
Async 关键字的用途
很容易忘记在使用await的方法签名中添加async关键字,但这是必需的。但是,这样做是有充分理由的。在 C# 5.0 中引入await关键字之前,您可以将await用作变量等的标识符名称。虽然在许多情况下,编译器可以根据上下文辨别预期用途,但为了保持完全的向后兼容性,引入了async关键字以消除歧义。如果方法签名中存在async关键字,则编译器知道将await解释为 C# 关键字。相反,如果不存在async关键字,则编译器允许您将变量命名为 await(如果您愿意的话)。
综上所述,可以理解的是,并非每个返回Task的方法都需要在其签名中使用async关键字。同样,仅当在特定方法的代码中使用await时,方法签名中才需要async。您可以创建并返回任务而不等待它们;这样做的方法不会使用async关键字。但不要太过强调细微差别!通常,编译器可以弄清楚您的意思,并提醒您在必要时添加async,或者在您不等待任何内容时将其删除。
您可能看到“async”一词的另一种情况是方法名称本身。例如,在System.Net.Http命名空间中的HttpClient类中,有一个具有以下签名的方法:
public Task<string> GetStringAsync (string requestUri)
重要的是要知道,在这种情况下,“Async”不是一个关键字,而只是一个命名约定;它对编译器没有任何意义。他们本可以很容易地将方法命名为GetString,并且它将以完全相同的方式运行。您可以随意在自己的异步方法后加上“Async”后缀。有些人认为这种命名约定很有用,而另一些人则认为它很分散注意力。请自行决定;这完全是可选的!
Async/Await的优点
除了我们上面谈到的代码可读性的巨大改进之外,使用async / await方法还有许多有趣的优势,使其更具吸引力。如果您将Task与ContinueWith一起使用,则抛出的异常不会逃离任务;您必须在任务完成时检查其属性,包括已完成的Task向您公开的AggregateException的InnerExceptions属性。相比之下,当您等待方法时,您可以正常使用try / catch,它会像预期的那样捕获异常。不仅如此,捕获的任何异常都将自动“解包”为实际异常,而不是任何类型的聚合持有者。因此,您可以安全地捕获 (FormatException),或任何可能的情况。
此外,如果您将Task与ContinueWith结合使用,则需要了解第一个任务使用的TaskScheduler,以便充分了解后续延续任务默认如何运行。或者,您可以明确指定要用于延续任务的调度程序,但这样做会增加代码的冗余度。这是一个高级主题,但可以说,人们可能会遇到一些棘手的问题,而使用async / await方法可以避免这些问题。如果您没有做任何特别不寻常的事情,那么使用await要简单得多。
权衡与结论
Of course, there are some trade-offs when using async/await, but the same can be said for asynchronous programming in general. If you compare asynchronous programming to synchronous programming, asynchronous programming gives you increased responsiveness at the expense of increased complexity, higher memory usage, and longer overall execution time. Using async/await does indeed have these drawbacks. In particular, the state machine it uses adds a decent amount of overhead. But for most C# developers, the benefits of async/await make it well worth the cons.
As you begin us
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~