C# async/await
关键术语
异步编程模型
- 异步编程是指在执行长时间运行的操作时,允许程序继续执行其他任务而不必等待该操作完成。在传统的同步编程中,当一个操作进行时,程序会一直等待直到它完成,这可能会导致阻塞并降低程序的响应性。
- 异步编程模型通过使用异步方法来执行这些长时间运行的操作,允许程序在等待操作完成的同时执行其他任务。
async/await关键字
- async关键字用于定义异步方法,这些方法可以包含await表达式。
- await表达式用于等待异步操作完成。当程序执行到await表达式时,它会暂时返回控制权给调用方,允许调用方执行其他任务。一旦异步操作完成,程序将在await表达式处继续执行。
- 异步方法可以在不阻塞主线程的情况下执行耗时的操作,从而提高程序的响应性。
编译器和运行时的处理
- 当编译器遇到async关键字时,它会将异步方法转换为状态机。这个状态机定义了异步方法的执行过程,并在异步操作完成时继续执行方法的剩余部分。
- 当编译器遇到await表达式时,它会生成代码来暂停当前方法的执行,并在异步操作完成后继续执行。这通常涉及到将异步操作注册到一个任务队列,并将方法的执行流程保存为状态。
Task和Task<T>
- 异步方法通常返回Task或Task<T>对象,这些对象表示异步操作的执行状态和结果。Task对象用于表示没有返回值的异步方法,而Task<T>表示具有返回值的异步方法。
- Task对象可以被等待,以便在操作完成后获取结果或处理异常。
同步上下文和异步上下文
- 在异步编程中,上下文的概念非常重要。同步上下文是指在哪个线程上执行代码,而异步上下文则是指异步操作完成后恢复执行的线程。
- await表达式默认会捕获当前上下文,并在异步操作完成后尝试将执行恢复到原来的上下文中。这确保了在UI应用程序中,异步操作完成后可以正确地在UI线程上更新UI元素。
async/await关键字主要用于简化异步编程,使得异步操作的编写和调用更加直观和易于理解。
-
async/await: async和await是C#中的关键字,用于声明异步方法和等待异步操作的完成。通过async关键字声明的方法可以包含await表达式,这样的方法称为异步方法。在异步方法中,可以使用await关键字等待另一个异步操作的完成,而不会阻塞当前线程。async/await机制使得异步代码的编写和调用更加直观和简单。
-
Task: Task是.NET Framework中表示异步操作的抽象。它代表了一个异步操作的执行状态和结果。async/await关键字通常与Task一起使用,用于管理和等待异步操作的完成。异步方法可以返回Task、Task<TResult>或void类型,其中void类型并不常用,因为无法获取异步方法的执行结果。
await前后代码的状态
await前:
- 当程序执行到await关键字之前,它在当前线程上执行同步代码。
- 如果这段同步代码执行完毕后,程序遇到了一个异步操作,它会生成一个任务(Task)来表示这个异步操作,并立即返回这个任务,允许当前线程继续执行其他任务或者返回到线程池中等待新的任务。
await期间:
- 一旦程序执行到await关键字,它会暂时返回控制权给调用方,并将当前的上下文(包括线程上下文)保存起来。
- 框架会等待异步操作完成,但不会阻塞当前线程。这意味着当前线程可以继续执行其他任务,或者返回到线程池中等待新任务。
- 当异步操作完成时,框架会尝试恢复原来的上下文,以便在原来的线程上继续执行后续代码。这通常会导致从线程池中获取一个线程来执行后续代码,但并不保证一定会使用之前的线程。
await后:
- 一旦异步操作完成,框架会从线程池中取出一个线程来执行await后面的代码。
- 执行await后面的代码时,它会在之前保存的上下文中恢复执行,以确保后续代码在正确的上下文中执行(例如在UI线程中更新UI)。
总的来说,await关键字的作用是在异步操作进行时暂时释放当前线程,允许程序执行其他任务,从而提高程序的并发性和响应性。当异步操作完成后,它会尝试将执行流程恢复到之前的上下文中,以便在正确的上下文中继续执行后续代码。
async的原理
异步方法在编译器中的实现主要依赖于状态机(state machine)。状态机是一种抽象的计算模型,它可以表示一个有限个状态以及在这些状态之间的转移。在异步方法中,状态机用于控制异步操作的执行流程,并在异步操作完成后正确地恢复执行。
生成状态机类
- 当编译器遇到一个async方法时,它会将该方法编译成一个类,这个类实现了一个自动生成的状态机。
- 这个类通常会被命名为原方法名 + "AsyncStateMachine",例如MyMethodAsyncStateMachine。
状态和状态转移
- 异步方法的执行过程被拆分为多个状态,每个状态代表方法的一个部分。这些状态通过字段或者枚举类型来表示。
- 编译器会根据await关键字将方法切分为多个状态,并生成对应的状态转移逻辑。
- 状态机类中通常会包含一个或多个MoveNext方法,这些方法用于执行状态之间的转移。当异步操作完成后,状态机会根据编译器生成的逻辑进行状态转移。
await表达式的处理
- 每次遇到await关键字时,编译器会生成代码来暂停当前状态机的执行,并在异步操作完成后继续执行。
- 在状态机类中,编译器会为每个await表达式生成相应的代码来等待异步操作的完成,并在完成后将执行流程恢复到之前的状态。
异步方法的调用
- 当调用一个异步方法时,实际上是在调用生成的状态机类的实例,并调用它的MoveNext方法开始执行。
- 状态机会根据编译器生成的逻辑执行相应的操作,包括等待异步操作完成、执行后续代码等。
通过状态机的实现,编译器能够将异步方法的执行流程拆分为多个状态,并在异步操作完成后正确地恢复执行。这种实现方式使得异步方法可以在不阻塞调用线程的情况下执行异步操作,并保持代码的简洁性和可读性。
async方法优缺点
异步方法具有许多优点和一些限制。让我们来看一下它们的优缺点
优点
- 提高性能和并发性: 异步方法允许在执行长时间运行的操作时释放线程,并在等待操作完成时执行其他任务。这提高了程序的响应性和吞吐量,因为它允许利用系统资源更有效地执行并发任务。
- 改善用户体验: 在GUI应用程序中,异步方法可以确保UI保持响应,并且不会因为等待I/O操作而冻结或阻塞。这可以提升用户体验,让用户感觉应用程序更流畅。
- 避免阻塞: 在Web服务器应用程序中,异步方法可以避免由于长时间的数据库查询或外部服务调用而导致的线程阻塞。这有助于提高Web服务器的吞吐量和性能。
- 节省资源: 使用异步方法可以减少线程的使用,因为它们允许在等待异步操作时释放线程。这可以节省系统资源,并降低内存和CPU的消耗。
缺点
- 生成额外的类和状态机:异步方法在编译过程中会被转换成状态机类,这可能会增加代码的复杂性,并在运行时引入一些额外的开销。生成的状态机类可能会增加代码大小和内存占用。
- 运行效率相对较低:异步方法的执行涉及到状态机的调度和状态转移,这可能会引入一些额外的性能开销。相对于同步方法而言,异步方法可能会有一些微小的性能损失。
- 可能会占用大量线程:如果异步方法内部存在大量的异步操作,而这些操作又都是通过Task.Run或其他方式在不同的线程上执行的,那么就可能会占用大量的线程资源。这可能会导致线程池中的线程资源被耗尽,从而影响系统的性能和稳定性。
- 调试和理解的复杂性:异步方法可能会增加代码的复杂性,使得调试和理解代码变得更加困难。特别是当异步方法中存在多个await表达式时,程序的执行流程可能会变得更加难以理解和追踪。
- 错误处理困难: 异步代码中的错误处理比同步代码更加困难,因为异步操作通常是非阻塞的,异常可能会在不同的上下文中发生。正确处理异步操作中的异常是一个挑战。
注意
-
对于仅仅调用其他异步方法且不需要额外处理结果的异步方法,考虑移除async关键字并改为同步方法(主要遇到async编译器仍会生成异步状态机以管理方法的执行流程),以减少异步状态机带来的额外开销。
-
在异步方法中避免使用Thread.Sleep(),因为它会阻塞调用线程,与异步方法的并发性和响应性相悖,推荐使用Task.Delay()来实现异步等待,因为它创建一个延迟任务而不会阻塞当前线程,允许其他任务或操作继续执行。