在软件开发中,异步编程是一种处理I/O密集型任务(如文件读写、网络通信等)的编程模型,它可以提高应用程序的响应性和性能。C# 语言提供了丰富的异步编程支持,其中最核心的就是 async 和 await 关键字。本文将详细介绍 C# 异步方法的运行机制。
1. 异步方法的基本概念以及其必要性
异步方法是一种允许代码在等待某个操作完成时继续执行其他任务的编程方法。必要性在于,许多操作(如文件I/O、网络请求等)可能需要较长时间才能完成,如果使用同步方法,程序将会在这些操作执行期间阻塞,导致用户界面无响应或服务不可用。异步方法可以让程序在等待这些操作的同时继续处理其他任务,从而提高应用程序的性能和用户体验。
2. 异步方法的声明与使用
在 C# 中,异步方法通过在方法声明前添加 async 关键字来标识。异步方法返回一个 Task 类型,而不是直接返回一个值。使用 await 关键字可以轻松地等待异步方法的结果。
public async Task<int> CalculateSquareRootAsync(double number)
{
return await Task.Run(() => Math.Sqrt(number));
}
3. 异步方法的参数传递机制
异步方法可以接受参数,这些参数可以在方法内部使用,但不能直接在 await 表达式中使用。这是因为 await 表达式在方法被调用时就已经完成了参数的传递,而在 await 表达式执行时,方法的主体还没有执行。
public async Task<int> CalculateSquareRootAsync(double number, CancellationToken cancellationToken)
{
return await Task.Run(() => Math.Sqrt(number), cancellationToken);
}
4. 异步方法执行的机制,包括异步事件日志
异步方法的执行机制涉及到 Task 类的使用。当一个异步方法被调用时,它会返回一个 Task 对象,这个对象代表了异步操作的状态。在异步方法内部,可以使用 await 关键字来等待其他 Task 的完成。
异步事件日志是一种记录异步操作执行情况的方法。可以通过捕获和记录 Task 相关的异常和完成事件来监控异步操作的执行情况。
public async Task<int> CalculateSquareRootAsync(double number)
{
try
{
return await Task.Run(() => Math.Sqrt(number));
}
catch (Exception ex)
{
// 记录异常
}
finally
{
// 记录完成事件
}
return 0;
}
5. 异步方法与其他类型的方法的区别
异步方法与同步方法的主要区别在于它们如何处理并发和等待操作。同步方法会在执行操作时阻塞当前线程,直到操作完成。而异步方法不会阻塞当前线程,可以在等待操作完成的同时执行其他任务。
此外,异步方法与事件驱动编程模型(如回调函数)也有所不同。异步方法通过 Task 对象来管理异步操作的状态,而事件驱动编程模型通常使用回调函数来响应事件。
6. 异步方法的运行机制
异步编程中最需弄清的是控制流是如何从方法移动到方法的。 下图可引导你完成此过程:
关系图中的数字对应于以下步骤,在调用方法调用异步方法时启动。
-
调用方法调用并等待 GetUrlContentLengthAsync 异步方法。
-
GetUrlContentLengthAsync 可创建 HttpClient 实例并调用 GetStringAsync 异步方法以下载网站内容作为字符串。
-
GetStringAsync 中发生了某种情况,该情况挂起了它的进程。 可能必须等待网站下载或一些其他阻止活动。 为避免阻止资源,GetStringAsync 会将控制权出让给其调用方 GetUrlContentLengthAsync。
GetStringAsync 返回 Task,其中 TResult 为字符串,并且 GetUrlContentLengthAsync 将任务分配给 getStringTask 变量。 该任务表示调用 GetStringAsync 的正在进行的进程,其中承诺当工作完成时产生实际字符串值。
-
由于尚未等待 getStringTask,因此,GetUrlContentLengthAsync 可以继续执行不依赖于 GetStringAsync 得出的最终结果的其他工作。 该任务由对同步方法 DoIndependentWork 的调用表示。
-
DoIndependentWork 是完成其工作并返回其调用方的同步方法。
-
GetUrlContentLengthAsync 已运行完毕,可以不受 getStringTask 的结果影响。 接下来,GetUrlContentLengthAsync 需要计算并返回已下载的字符串的长度,但该方法只有在获得字符串的情况下才能计算该值。
因此,GetUrlContentLengthAsync 使用一个 await 运算符来挂起其进度,并把控制权交给调用 GetUrlContentLengthAsync 的方法。 GetUrlContentLengthAsync 将 Task 返回给调用方。 该任务表示对产生下载字符串长度的整数结果的一个承诺。
备注
如果 GetStringAsync(因此 getStringTask)在 GetUrlContentLengthAsync 等待前完成,则控制会保留在 GetUrlContentLengthAsync 中。 如果异步调用过程 getStringTask 已完成,并且 GetUrlContentLengthAsync 不必等待最终结果,则挂起然后返回到 GetUrlContentLengthAsync 将造成成本浪费。
在调用方法中,处理模式会继续。 在等待结果前,调用方可以开展不依赖于 GetUrlContentLengthAsync 结果的其他工作,否则就需等待片刻。 调用方法等待 GetUrlContentLengthAsync,而 GetUrlContentLengthAsync 等待 GetStringAsync。
-
GetStringAsync 完成并生成一个字符串结果。 字符串结果不是通过按你预期的方式调用 GetStringAsync 所返回的。 (记住,该方法已返回步骤 3 中的一个任务)。相反,字符串结果存储在表示 getStringTask 方法完成的任务中。 await 运算符从 getStringTask 中检索结果。 赋值语句将检索到的结果赋给 contents。
-
当 GetUrlContentLengthAsync 具有字符串结果时,该方法可以计算字符串长度。 然后,GetUrlContentLengthAsync 工作也将完成,并且等待事件处理程序可继续使用。 在此主题结尾处的完整示例中,可确认事件处理程序检索并打印长度结果的值。 如果你不熟悉异步编程,请花 1 分钟时间考虑同步行为和异步行为之间的差异。 当其工作完成时(第 5 步)会返回一个同步方法,但当其工作挂起时(第 3 步和第 6 步),异步方法会返回一个任务值。 在异步方法最终完成其工作时,任务会标记为已完成,而结果(如果有)将存储在任务中。
示例代码
下面是一个异步方法的示例,它计算一个数的平方根,并使用 await 关键字等待计算完成。
using System;
using System.Threading.Tasks;
class Program
{
public static void Main()
{
var result = CalculateSquareRootAsync(4.0);
Console.WriteLine("计算平方根...");
result.Wait(); // 等待异步操作完成
Console.WriteLine("平方根为: " + result.Result);
}
public static async Task<int> CalculateSquareRootAsync(double number)
{
return await Task.Run(() => Math.Sqrt(number));
}
}
在示例代码中,CalculateSquareRootAsync 方法是一个异步方法,它使用 await 关键字来等待 Task.Run 的完成。在 Main 方法中,我们调用 CalculateSquareRootAsync 方法并使用 Wait 方法来等待异步操作完成。Wait 方法会导致当前线程阻塞,直到 CalculateSquareRootAsync 方法返回结果。
总结
C# 的异步编程模型通过 async 和 await 关键字提供了一种简洁、易于理解和使用的异步编程方式。它利用 Task 类和后台线程池来管理异步操作,从而提高了应用程序的响应性和性能。异步方法可以处理 I/O 密集型任务,避免阻塞主线程,同时允许程序在等待操作完成时执行其他任务。通过理解异步方法的基本概念、声明和使用方式,开发者可以更有效地编写高质量的应用程序。