由浅到深 谈.NET的Async、Await关键字

由浅到深 谈.NET的Async、Await关键字
最新回答
汏姐萌神

2022-11-14 13:12:53

由浅到深谈.NET的Async、Await关键字

在.NET编程中,async和await是用于简化异步编程的关键字。它们使得编写异步代码更加直观,减少了传统异步编程模型(如回调或事件)的复杂性。下面将从基础概念到深入原理,逐步探讨这两个关键字。

1. 基础概念
  • async关键字:用于标记一个方法为异步方法。它告诉编译器,这个方法可能包含await表达式,并且需要特殊处理以支持异步操作。

  • await关键字:用于暂停异步方法的执行,直到等待的异步操作完成。它只能在async方法中使用,并且通常用于等待Task或Task<TResult>的完成。

2. async关键字详解
  • 标识异步方法

    使用async标记的方法并不自动成为异步执行的。它只是告诉编译器,这个方法内部可能包含await表达式,需要编译器生成额外的状态机代码来支持异步操作。

    示例:

    public static async Task<int> asyncMethod(){ return await Task.Run(() => Calculate());}static int Calculate(){ return 1 + 1;}在这个例子中,asyncMethod被标记为async,并且内部使用了await来等待Task.Run的完成。
  • 编译器处理

    当方法被async标记时,编译器会生成一个状态机来管理方法的执行流程。这个状态机允许方法在遇到await时暂停执行,并在异步操作完成后恢复执行。

    编译器还会确保方法的返回值(如果是Task或Task<TResult>)被正确处理,包括异常传播。

  • 同步与异步的边界

    使用async标记的方法本身并不决定其执行方式是同步还是异步。真正的异步行为是由await关键字触发的。

    如果一个async方法中没有await表达式,那么该方法将完全同步执行,尽管它仍然被标记为async。

  • 参数限制

    async方法不能使用ref或out参数。这是因为这些参数的值可能在异步操作完成前后发生变化,导致不可预测的行为。

3. await关键字详解
  • 等待异步操作

    await用于暂停当前方法的执行,直到等待的异步操作(通常是Task或Task<TResult>)完成。

    示例:

    await Task.Run(() => "xpy0928");在这个例子中,await会等待Task.Run完成,并返回其结果(一个字符串)。
  • 状态机与继续执行

    当执行到await时,编译器生成的状态机会检查异步操作是否已完成。如果已完成,则继续执行await之后的代码;否则,状态机会挂起,并在异步操作完成后恢复执行。

    这个过程是通过TaskAwaiter结构体实现的,它提供了IsCompleted属性来检查任务是否完成,以及OnCompleted方法来注册继续执行的回调。

  • 自定义异步等待

    可以通过实现GetAwaiter方法为自定义类型提供异步等待支持。这允许开发者扩展异步编程模型,使其适用于非Task类型的异步操作。

    示例:

    public static class Extend{ public static TaskAwaiter GetAwaiter(this TimeSpan timeSpan) { return Task.Delay(timeSpan).GetAwaiter(); }}在这个例子中,为TimeSpan类型添加了一个GetAwaiter方法,使得可以使用await来等待一个时间间隔。
4. 异步Lambda表达式
  • 基本用法

    异步Lambda表达式是包含await表达式的Lambda表达式。它们可以用于需要异步操作的场景,如事件处理、LINQ查询等。

    示例:

    var task = Task.Run(async () =>{ await Task.Delay(TimeSpan.FromMilliseconds(5000));});在这个例子中,Lambda表达式被标记为async,并且内部使用了await来等待一个延迟。
  • 限制与注意事项

    异步Lambda表达式在转换为表达式树时可能会受到限制。特别是,有参数的异步Lambda表达式可能无法直接转换为表达式树,导致编译错误。

    在某些情况下,可以通过调整方法签名或使用其他模式(如返回Task的方法)来绕过这些限制。

5. 深入原理与状态机
  • 状态机生成

    当编译器遇到一个async方法时,它会生成一个状态机类来管理方法的执行流程。这个状态机类包含了方法的所有局部变量和状态信息,以及用于恢复执行的方法(如MoveNext)。

    状态机通过switch语句和状态字段(如$state)来跟踪方法的执行位置,并在异步操作完成后恢复执行。

  • 异常处理

    在async方法中,异常会被捕获并存储在返回的Task对象中。当调用方使用await等待该Task时,异常会被重新抛出,从而允许调用方处理异常。

    这种机制确保了异步方法中的异常能够像同步方法中的异常一样被传播和处理。

6. 最佳实践与常见误区
  • 避免async void

    除了事件处理程序外,应尽量避免使用async void方法。这是因为async void方法中的异常无法被捕获和处理,可能导致应用程序崩溃。

    相反,应使用async Task或async Task<TResult>方法,它们允许调用方使用await来等待方法完成并处理异常。

  • 合理使用await

    不是所有的异步操作都需要立即使用await等待。在某些情况下,可以通过返回Task或Task<TResult>来让调用方决定何时等待异步操作的完成。

    这种模式可以提高代码的灵活性和可重用性。

  • 理解异步与并行的区别

    异步编程并不等同于并行编程。异步操作可以在单个线程上执行,通过挂起和恢复执行来实现非阻塞的I/O操作或其他长时间运行的操作。

    并行编程则涉及多个线程同时执行代码,以利用多核处理器的优势。虽然异步编程可以与并行编程结合使用,但它们是两个不同的概念。

7. 总结与展望
  • 简化异步编程

    async和await关键字极大地简化了.NET中的异步编程模型。它们使得编写异步代码更加直观和易于理解,减少了传统异步编程模型的复杂性。

  • 持续演进

    随着.NET平台的不断发展,异步编程模型也在持续演进。新的API和语言特性不断涌现,为开发者提供了更多选择和灵活性。

    例如,C# 8.0引入了异步流(IAsyncEnumerable<T>),允许开发者使用await foreach来异步迭代数据流。

  • 深入理解原理

    虽然async和await关键字使得异步编程更加简单,但深入理解其背后的原理(如状态机、任务并行库等)仍然非常重要。

    这有助于开发者编写更高效、更可靠的异步代码,并避免常见的陷阱和误区。