C# 5.0 使用 async 和 await 进行异步编程
一篇关于 C# 5 和 .NET 4.5 新特性的轻量级文章
引言
在 2010 年 3 月发布的一篇题为“扩展 IAsyncResult 模式以支持多参数函数调用”的文章中,我展示了如何实现 IAsynchResult
来解决当时遇到的一个问题。如果当时支持这项功能,我就不必这么做了。
我现在将介绍 C# 5.0 中引入的一项新功能,它使用了两个关键字 async
和 await
。
为什么它很重要
正如大家所知,异步和并行编程是一种非常重要的编程风格,主要旨在完善我们应用程序的响应能力。.NET 自发布以来一直致力于支持这种风格,并且在每个版本中都不断推出新功能和新的异步方式。所有这些功能都超出了本文的范围,但有很多资源以清晰详尽的方式讨论了它们,并详细描述了它们的使用方法。
然而,我们开发者习惯于编写方法,我认为是同步方法,并尝试通过多种方式(Thread
, ThreadStart
, ThreadPool
, BackgroundWorker
等)异步调用它们,但从本质上讲,编写异步方法既困难又难以维护。
我将要介绍的功能使我们能够轻松地即时创建异步方法,事实上,它帮助我们将传统的同步方法转变为异步方法。
任务类
该功能可以通过以下示例进行总结,通过简单地更改返回类型,我们就将方法本身变成了固有的或本质上的异步方法。
让我们考虑一个耗时的方法
public static IEnumerable<int> getPrimes(int min, int count)
{
return Enumerable.Range(min, count).Where
(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i =>
n % i > 0));
}
根据参数 min
和 count
,此方法可能需要很长时间。
使其异步的一种方法是简单地更改返回类型,如下例所示
public static Task<IEnumerable<int>> getPrimesAsync(int min, int count)
{
return Task.Run (()=> Enumerable.Range(min, count).Where
(n => Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i =>
n % i > 0)));
}
在上面的示例中,请注意我们将方法本身的名称通过添加 Async 来进行了更改,这是应该遵循的约定。
对于原本为 void
的方法,返回类型为 Task
;对于原本返回类型为 T
的方法,返回类型为 Task<T>
。
什么是 Task?
Task 简单来说就是 IAsynchResult
的一个实现,这就是为什么我必须提到我之前写的文章或我遇到的问题以及我不得不达到的解决方案。
现在,当一个方法返回 Task<T>
时,它就是可等待的(awaitable),这意味着你可以使用 await
关键字来调用它,这意味着每当你使用 await
调用它时,执行控制将立即返回给你,而不会对你的应用程序响应能力产生任何影响。
让我们看看如何调用上面的这些方法
private static void PrintPrimaryNumbers()
{
for (int i = 0; i < 10; i++)
getPrimes(i * 100000 + 1, i * 1000000)
.ToList().
ForEach(x => Trace.WriteLine(x));
}
PrintPrimaryNumbers()
是一个我们可以直接传统调用的方法。我在这里调用它 10 次,它将依次调用,我们将看到它完成需要多长时间。
private static async void PrintPrimaryNumbersAsync()
{
for (int i = 0; i < 10; i++)
{
var result = await getPrimesAsync(i * 100000 + 1, i * 1000000);
result.ToList().ForEach(x => Trace.WriteLine(x));
}
}
而 PrintPrimaryNumbersAsync()
被 async
关键字修饰,它异步调用 getPrimesAsync
。
一旦调用,执行立即返回给调用者(主线程)。一旦其中一个其他线程完成,它将重新获得控制(在本例中,将找到的素数写入提供的范围内)。
为了让画面更清晰,请运行并缩小范围,就像附加的源代码文件一样。
现在让我们看看 main
函数
static void Main(string[] args)
{
DateTime t1 = DateTime.Now;
PrintPrimaryNumbers();
var ts1 = DateTime.Now.Subtract(t1);
Trace.WriteLine("Finished Sync and started Async");
var t2 = DateTime.Now;
PrintPrimaryNumbersAsync();
var ts2 = DateTime.Now.Subtract(t2);
Trace.WriteLine(string.Format(
"It took {0} for the sync call and {1} for the Async one", ts1, ts2));
Trace.WriteLine("Any Key to terminate!!");
}
你能区分 ts1
和 ts2
吗?
结果如下
Finished Sync and started Async
It took 00:32:16.1627422 for the sync call and 00:00:00.0050003 for the Async one
如果你选择一个较小的范围进行测试,你可能会看到类似以下的内容
非常重要的是要注意,这里测量的不是异步操作完成所需的时间,而是异步操作启动或开始所需的时间,但它并没有阻塞主线程,这就是它的妙处。如果你等待这个操作并等待结果返回,然后测量时间,你就会准确地知道异步调用所花费的时间。
通常我们使用 Task.WaitAny(...)
或 Task.WaitAll(...)
来让主线程等待,直到异步调用完成,而这正是我在这里没有做到的,也许应该做。
Console.ReadLine()
会保持主线程不退出,直到你按下任意键,期望你会等待看到结果再终止它。
总之,该示例向你展示了你可以轻松地运行异步调用而不会阻塞主线程,但它并没有展示如何以适当的方式获取结果。
摘要
.NET 平台 4.5 在异步编程方面做出了一些革命性的技术,并放弃了一些现在被称为过时的旧技术(BackgroundWorker
、事件异步编程、异步编程模型 APM)。
参考文献
- C# 5.0 概览:终极参考指南 [平装本]:Joseph Albahari (作者), Ben Albahari (作者)
- 扩展 IAsyncResult 模式以支持多参数函数调用