C# 异步和Await 编程模型从头开始






4.82/5 (9投票s)
用 C# 解释 async await 编程模型。
引言
本文是为希望了解基础知识和一些内部工作原理的普通开发人员,对 async
和 await
关键字的简要介绍。
背景
在开发任何应用程序时,异步编程现在都至关重要,因为它避免了主线程在磁盘 I/O、网络操作、数据库访问等耗时操作上等待。在正常情况下,如果我们的程序需要这些耗时操作的结果,那么我们的代码将一直阻塞,直到操作完成。
使用异步机制,我们可以触发耗时操作,同时执行其他任务。这些耗时操作在不同的线程上执行,完成后会通知我们的主代码,然后我们的代码可以从这里执行后续操作。当我们提到“我们的代码”时,是指处理用户界面或主要处理 Web 请求或服务 UI 的线程。有时,我们自己会编写这些耗时操作。
什么是 async 和 await
简单来说,它们是 .NET 4.5 中引入的两个新关键字,用于轻松编写异步程序。它们在方法级别工作。当然,我们不能让类并行工作,因为它们不是执行单元。
这些关键字是 CLR(.NET 运行时)已知的,还是 TPL(任务并行库)的包装器?如果它们是包装器,那么让语言依赖于用同一种语言编写的库是否明智?
在本文中,我们将尝试找出这些问题的答案。
.NET 异步编程的历史
线程从 .NET 框架一开始就存在。它们是操作系统线程的包装器,使用起来有些困难。然后出现了像后台工作者、async delegate
和任务并行库 (TPL) 等概念,以简化异步编程模型。这些是作为类库的一部分出现的。直到 async
和 await
关键字随 C# 4.0 一起引入,C# 语言本身并没有“开箱即用”的异步编程支持。让我们通过检查这些方法中的每一种来了解 async
和 await
如何帮助我们进行异步编程。
示例
让我们以找出前 N 个数中能被 3 整除的数的阶乘的示例如例。为简单起见,我们使用控制台应用程序。如果我们使用 Windows 应用程序,我们可以轻松地挂接到 async
事件委托处理程序并演示 async
功能。但这无助于我们学习语言特性。
同步代码
public void Main()
{
for (int counter = 1; counter < 5; counter++)
{
if (counter % 3 == 0)
{
WriteFactorial(counter);
}
else
{
Console.WriteLine(counter);
}
}
Console.ReadLine();
}
private void WriteFactorial(int no)
{
int result = FindFactorialWithSimulatedDelay(no);
Console.WriteLine("Factorial of {0} is {1}", no, result);
}
private static int FindFactorialWithSimulatedDelay(int no)
{
int result = 1;
for (int i = 1; i <= no; i++)
{
Thread.Sleep(500);
result = result * i;
}
return result;
}
我们可以看到一个循环,它使用计数器变量从 1 迭代到 5。它检查当前计数器值是否能被 3 整除。如果是,则写入其阶乘。写入函数通过调用 FindFactorialWithSimulatedDelay()
方法来计算阶乘。此方法在示例中将产生延迟以模拟真实工作负载。换句话说,这就是耗时操作。
我们可以很容易地看出,执行是按顺序进行的。循环中的 WriteFactorial()
调用会等待阶乘计算完成。为什么我们必须在这里等待?为什么不能转到下一个数字,因为数字之间没有依赖关系?我们可以。但是 WriteFactorial()
中的 Console.WriteLine
语句怎么办?它必须等待阶乘找到。这意味着我们可以异步调用 FindFactorialWithSimulatedDelay()
,前提是有一个回调到 WriteFactorial()
。当异步调用发生时,循环可以使计数器前进到下一个数字并调用 WriteFactorial()
。
线程是我们可以实现这一目标的一种方式。由于线程比普通开发人员更难使用,需要更多知识,因此我们使用了 async delegate
机制。以下是使用 async delegate
重写的 WriteFactorial()
方法。
使用 Async Delegates 使其异步化
早期使用的一个更简单的方法是使用异步委托调用。它使用 Begin
/End
方法调用机制。在这里,运行时使用线程池中的线程来执行代码,完成后我们可以获得回调。下面的代码很好地解释了这一点,它使用了 Func
委托。
private void WriteFactorialAsyncUsingDelegate(int facno)
{
Func<int, int> findFact = FindFactorialWithSimulatedDelay;
findFact.BeginInvoke(facno,
(iAsyncresult) =>
{
AsyncResult asyncResult = iAsyncresult as AsyncResult;
Func<int, int> del = asyncResult.AsyncDelegate as Func<int, int>;
int factorial = del.EndInvoke(asyncResult);
Console.WriteLine("Factorial of {0} is {1}", facno, factorial);
},
null);
}
public void Main()
{
for (int counter = 1; counter < 5; counter++)
{
if (counter % 3 == 0)
{
WriteFactorialAsyncUsingDelegate(counter);
}
else
{
Console.WriteLine(counter);
}
}
Console.ReadLine();
}
计算阶乘没有改变。我们只是添加了一个名为 WriteFactorialAsyncUsingDelegate()
的新函数,并修改了 Main
以在循环中调用此方法。
一旦调用了 findFact delegate
上的 BeginInvoke
,主线程就会返回到计数器循环,然后它会增加计数器并继续循环。当阶乘可用时,匿名回调将被触发,并将写入控制台。
缺点
我们没有直接取消任务的选项。此外,如果我们想等待一个或多个方法,会有些困难。
另外,我们可以看到代码块没有被包装成对象,我们需要与 IAsyncResult
对象进行斗争才能取回结果。TPL 也解决了这个问题,它看起来更面向对象。让我们看一下。
使用 TPL 改进异步编程
TPL 在 .NET 4.0 中引入。我们可以将异步代码包装在 Task
对象中并执行它。我们可以等待一个或多个任务完成。可以轻松取消任务等。还有更多内容。下面是用 TPL 重写我们的阶乘写入代码。
private void WriteFactorialAsyncUsingTask(int no)
{
Task<int> task=Task.Run<int>(() =>
{
int result = FindFactorialWithSimulatedDelay(no);
return result;
});
task.ContinueWith(new Action<Task<int>>((input) =>
{
Console.WriteLine("Factorial of {0} is {1}", no, input.Result);
}));
}
public void Main()
{
for (int counter = 1; counter < 5; counter++)
{
if (counter % 3 == 0)
{
WriteFactorialAsyncUsingTask(counter);
}
else
{
Console.WriteLine(counter);
}
}
Console.ReadLine();
}
在这里,我们可以看到第一个任务正在运行,然后它继续执行下一个任务,即已完成的处理程序,该处理程序接收第一个任务的通知并将其结果写入控制台。
缺点
这仍然不是语言特性。我们需要引用 TPL 库来获得支持。主要问题是编写完成事件处理程序的麻烦。让我们看看如何使用 async 和 await 关键字重写它。
语言特性 async 和 await
我们将看到如何使用 async
和 await
关键字重写 TPL 示例。我们使用 async
关键字装饰了 WriteFactorialAsyncUsingAwait
方法,以表示该函数将以 async
方式执行操作,并且可能包含 await
关键字。没有 async
,我们就不能 await。
然后,我们等待阶乘查找函数。在执行过程中遇到 await
的那一刻,线程会转到调用方法并从那里恢复执行。在我们的例子中,是计数器循环,然后取下一个数字。被等待的代码使用 TPL 作为其任务执行。像往常一样,它从线程池中获取一个线程并执行它。一旦执行完成,将执行 await
下面的语句。
在这里,我们也**不会**更改 FindFactorialWithSimulatedDelay()
中的任何内容。
private async Task WriteFactorialAsyncUsingAwait(int facno)
{
int result = await Task.Run(()=> FindFactorialWithSimulatedDelay(facno));
Console.WriteLine("Factorial of {0} is {1}", facno, result);
}
public void Main()
{
for (int counter = 1; counter < 5; counter++)
{
if (counter % 3 == 0)
{
WriteFactorialAsyncUsingAwait(counter);
}
else
{
Console.WriteLine(counter);
}
}
Console.ReadLine();
}
这避免了对额外回调处理程序的需求,开发人员可以以顺序方式编写代码。
Task Parallel Library 和 async await 关键字的关系是什么?
async
和 await
关键字在内部利用 TPL。更确切地说,我们可以说 async
和 await
是 C# 语言中的语法糖。仍然不清楚?换句话说,.NET 运行时不知道 async
和 await
关键字。
查看 WriteFactorialAsyncUsingAwait()
的反汇编代码。您可以使用 Reflector 或类似工具来反汇编程序集。
何时使用它?
每当我们等待某些内容时,即每当我们处理异步场景时,都可以使用 async 和 await。例如,文件 I/O、网络操作、数据库操作等。这将帮助我们使 UI 响应。
关注点
有趣的问题是:语言是否应该依赖于用它创建的库/类?语言是否应该了解并行编程构造?最后,编译器是否应该修改我们的代码,以便在调试代码时能看到一些新的东西?
历史
- 2016 年 3 月 6 日:首次发布