【C# TAP 异步编程】三、async\await的运作机理详解 【2022年原创】
【原创】 本文只是个人笔记,很多错误,欢迎指出。
环境:vs2022 .net6.0 C#10
async\await的代码运行顺序
通过分析一段代码运行来了解async\await的运作机理(本案例是控制台应用程序)
namespace MyTask; class Program { public static void Main(string[] args) { Task<int> baconTask = GetUrlContentLengthAsync(3); baconTask.ContinueWith(t => Console.WriteLine(t.Result)); Console.Read(); } static async Task<int> GetUrlContentLengthAsync(int slices) { HttpClient httpClient = new(); //********* 也可以合并起来写*********// // string content = await httpClient.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html");
//将生成TaskAwaiter的等待器,该等待器继承 ITaskAwaiter Task<string> getContent= httpClient.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html"); string content = await getContent; //await getContent反编译后是awaiter =5__2.GetAwaiter(); DoInDependentWork(); return content.Length; } static void DoInDependentWork() { Console.WriteLine($"content.Length:Working"); } }
备用 改进后的代码,输出线程的变化
using System.Runtime.CompilerServices; namespace MyTask; class Program { public static void Main(string[] args) { Console.WriteLine("mian方法在异步方法 标志之前的线程:{0}", Environment.CurrentManagedThreadId); Task<int> baconTask = GetUrlContentLengthAsync(3); // baconTask.ContinueWith(t => Console.WriteLine(t.Result)); baconTask.Wait(); Console.WriteLine("mian方法在异步方法 标志之后的线程:{0}", Environment.CurrentManagedThreadId); Console.Read(); } static async Task<int> GetUrlContentLengthAsync(int slices) { Console.WriteLine("异步方法在await 标志之前的线程:{0}", Environment.CurrentManagedThreadId); HttpClient httpClient = new(); // string content = await httpClient.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html"); Task<string> getContent = httpClient.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html"); string content = await getContent; //await getContent反编译后是awaiter =5__2.GetAwaiter(); Console.WriteLine("异步方法在await 标志之后的线程:{0}",Environment.CurrentManagedThreadId); return content.Length; } static void DoInDependentWork() { Console.WriteLine($"content.Length:Working"); } } /* 输出 mian方法在异步方法 标志之前的线程:1 异步方法在await 标志之前的线程:1 异步方法在await 标志之后的线程:10 mian方法在异步方法 标志之后的线程:1 */
IL反编译后的代码
// MyTask.Program using System; using System.Diagnostics; using System.Net.Http; using System.Runtime.CompilerServices; using System.Threading.Tasks; using MyTask; [System.Runtime.CompilerServices.NullableContext(1)] [System.Runtime.CompilerServices.Nullable(0)] internal class Program { [Serializable] [CompilerGenerated] private sealed class <>c { [System.Runtime.CompilerServices.Nullable(0)] public static readonly <>c <>9 = new <>c(); [System.Runtime.CompilerServices.Nullable(new byte[] { 0, 1 })] public static Actionint>> <>9__0_0; internal void b__0_0(Task<int> t) { Console.WriteLine(t.Result); } } [CompilerGenerated] private sealed class d__1 : IAsyncStateMachine { public int <>1__state; [System.Runtime.CompilerServices.Nullable(0)] public AsyncTaskMethodBuilder<int> <>t__builder; public int slices; [System.Runtime.CompilerServices.Nullable(0)] private HttpClient 5__1; [System.Runtime.CompilerServices.Nullable(new byte[] { 0, 1 })] private Task<string> 5__2; [System.Runtime.CompilerServices.Nullable(0)] private string 5__3; [System.Runtime.CompilerServices.Nullable(0)] private string <>s__4; [System.Runtime.CompilerServices.Nullable(new byte[] { 0, 1 })] private TaskAwaiter<string> <>u__1; private void MoveNext() { int num = <>1__state; int length; try { TaskAwaiter<string> awaiter; if (num != 0) { 5__1 = new HttpClient(); 5__2 = 5__1.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html"); awaiter = 5__2.GetAwaiter(); if (!awaiter.IsCompleted) { num = (<>1__state = 0); <>u__1 = awaiter; d__1 stateMachine = this; <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); return; } } else { awaiter = <>u__1; <>u__1 = default(TaskAwaiter<string>); num = (<>1__state = -1); } <>s__4 = awaiter.GetResult(); 5__3 = <>s__4; <>s__4 = null; DoInDependentWork(); length = 5__3.Length; } catch (Exception exception) { <>1__state = -2; 5__1 = null; 5__2 = null; 5__3 = null; <>t__builder.SetException(exception); return; } <>1__state = -2; 5__1 = null; 5__2 = null; 5__3 = null; <>t__builder.SetResult(length); } void IAsyncStateMachine.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext this.MoveNext(); } [DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine this.SetStateMachine(stateMachine); } } public static void Main(string[] args) { Task<int> baconTask = GetUrlContentLengthAsync(3); baconTask.ContinueWith(<>c.<>9__0_0 ?? (<>c.<>9__0_0 = new Action int>>(<>c.<>9. b__0_0))); Console.Read(); } [AsyncStateMachine(typeof( d__1))] [DebuggerStepThrough] private static Task<int> GetUrlContentLengthAsync(int slices) { d__1 stateMachine = new d__1(); stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create(); stateMachine.slices = slices; stateMachine.<>1__state = -1; stateMachine.<>t__builder.Start(ref stateMachine); return stateMachine.<>t__builder.Task; } private static void DoInDependentWork() { Console.WriteLine("content.Length:Working"); } }
通过IL代码我们可以分析出整个代码运行过程 如下:
关系图中的数字对应于以下步骤,在调用方法调用异步方法时启动。
-
Main调用方法调用并等待
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 步),异步方法会返回一个任务值。 在异步方法最终完成其工作时,任务会标记为已完成,而结果(如果有)将存储在任务中。
以上内容来自:https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/task-asynchronous-programming-model
async\await的运行机理
反编译后我们可以看到async\await的运作机理主要分为分异步状态机和等待器,现在我主要来讲解着两部分的运行机制。
1、异步状态机
(1)异步状态机 始状态是-1;
(2)运行第一个【等待器】 期间异步状态机的状态是0。
(3)第一个【等待器】完成后异步状态机状态恢复-1。
(4)运行等二个【等待器】期间 异步状态机的状态是1,后面【等待器】以此类推。
(5)当所有的等待器都完成后,异步状态机的状态为-2。
异步状态机的组成
2、等待器
TaskAwaiter等待器
第一个案例中代码await httpClient.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html");生成TaskAwaiter类型等待器。该类型等待器继承ITaskAwaiter接口
(1)有返回值的等待器,它返回值就是异步方法public Task
此例子 中httpClient.GetStringAsync() 返回值是Task
所以等待器 TaskAwaiter的返回值是 string(Task
ConfiguredTaskAwaiter等待器
通过第 以下代码了解 等二种等待器类型
await httpClient.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html").ConfigureAwait(false);生成ConfiguredTaskAwaitable.ConfiguredTaskAwaiter类型等待器.
该等待器将被设置成bool m_continueOnCapturedContext属性被设置成fasle, 该等待器继承IConfiguredTaskAwaiter接口。
StateMachineBoxAwareAwaiter等待器
await Task.Yield();生成IStateMachineBoxAwareAwaiter类型等待器.,该类型的等待器继承自IStateMachineBoxAwareAwaiter接口
Task.Yield 简单来说就是创建时就已经完成的 Task ,或者说执行时间为0的 Task ,或者说是空任务。
我们知道在 await 时会释放当前线程,等所 await 的 Task 完成时会从线程池中申请新的线程继续执行 await 之后的代码,这本来是为了解决异步操作(比如IO操作)霸占线程实际却用不到线程的问题,而 Task.Yield 却产生了一个不仅没有异步操作而且什么也不干的 Task ,不是吃饱了撑着吗?
真正的图谋是借助 await 实现线程的切换,让 await 之后的操作重新排队从线程池中申请线程继续执行。
将不太重要的比较耗时的操作放在新的线程(重新排队从线程池中申请到的线程)中执行。
3、通过IL代码,查看异步的执行过程,以及最后是如何把任务交给线程池运行
异步代码大概运行顺序:
步骤1、主程序main调用异步f方法GetUrlContentLengthAsync()
步骤2、GetUrlContentLengthAsync();完成状态机初始化,然后调用状态机中异步任务方法构建器 中的开始函数:stateMachine.<>t__builder.Start(ref stateMachine);
步骤3、运行Start()函数中的AsyncMethodBuilderCore.Start(ref stateMachine);语句,保存当前线程的ExecutionContext(执行状态机)和SynchronizationContext(同步状态机),然后执行stateMachine.MoveNext();
该语句调用状态机的 MoveNext()方法;步骤4 开始进入状态机的MoveNext()方法;
步骤4、生成等待器 awaiter =
步骤5开始, 就是任务是一步一步被送入线程池运行,然后程序把控制权还给调用程序main。
步骤5、运行AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task);
步骤6、生成状态机盒子,将状态机和当前线程执行上下文 封装入状态机 代码如下:
IAsyncStateMachineBox stateMachineBox = GetStateMachineBox(ref stateMachine, ref taskField);
然后运行AwaitUnsafeOnCompleted(ref awaiter, stateMachineBox);
步骤7、执行以下代码。根据不同的等待器类型,选择进入askAwaiter.UnsafeOnCompletedInternal()方法中。
//第一种等待器 获取同步上下文 if (default(TAwaiter) != null && awaiter is ITaskAwaiter) { TaskAwaiter.UnsafeOnCompletedInternal(Unsafe.As(ref awaiter).m_task, box, true); return; } //第二种等待器 根据初始化设置决定要不要获取同步上下文 if (default(TAwaiter) != null && awaiter is IConfiguredTaskAwaiter) { ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter reference = ref Unsafe.As (ref awaiter); TaskAwaiter.UnsafeOnCompletedInternal(reference.m_task, box, reference.m_continueOnCapturedContext); return; } //第三种等待器 if (default(TAwaiter) != null && awaiter is IStateMachineBoxAwareAwaiter) { try { ((IStateMachineBoxAwareAwaiter)(object)awaiter).AwaitUnsafeOnCompleted(box); return; } catch (Exception exception) { System.Threading.Tasks.Task.ThrowAsync(exception, null); return; } }
TaskAwaiter.UnsafeOnCompletedInternal方法声明如下:
internal static void UnsafeOnCompletedInternal(Task task, IAsyncStateMachineBox stateMachineBox, bool continueOnCapturedContext)
步骤8、然后运行以下代码,进入 task.SetContinuationForAwait()方法中
internal static void UnsafeOnCompletedInternal(Task task, IAsyncStateMachineBox stateMachineBox, bool continueOnCapturedContext) { if (TplEventSource.Log.IsEnabled() || Task.s_asyncDebuggingEnabled) { task.SetContinuationForAwait(OutputWaitEtwEvents(task, stateMachineBox.MoveNextAction), continueOnCapturedContext, false); } else { task.UnsafeSetContinuationForAwait(stateMachineBox, continueOnCapturedContext); } }
运行OutputWaitEtwEvents(task, stateMachineBox.MoveNextAction,将任务和状态机的moveNext()封装成新的 委托。然后task.SetContinuationForAwait()运行该委托。
捕获当前同步上下文,
步骤9、运行以下代码, 这时候就用到了步骤7 提到的参数continueOnCapturedContext。由于步骤7传入是true 所以该方法就获取了SynchronizationContext同步上下文。
internal void SetContinuationForAwait(Action continuationAction, bool continueOnCapturedContext, bool flowExecutionContext) { TaskContinuation taskContinuation = null; if (continueOnCapturedContext) { SynchronizationContext current = SynchronizationContext.Current;//获取SynchronizationContext同步上下文,控制台程序为null,ui程序该项不为null if (current != null && current.GetType() != typeof(SynchronizationContext)) { //SynchronizationContext不等于null线程执行这一步 taskContinuation = new SynchronizationContextAwaitTaskContinuation(current, continuationAction, flowExecutionContext);//将同步上希望一起封装入了实例TaskContinuation中 } else { //SynchronizationContext等于null线程执行这一步 TaskScheduler internalCurrent = TaskScheduler.InternalCurrent; if (internalCurrent != null && internalCurrent != TaskScheduler.Default) { taskContinuation = new TaskSchedulerAwaitTaskContinuation(internalCurrent, continuationAction, flowExecutionContext); } } } if (taskContinuation == null && flowExecutionContext) { taskContinuation = new AwaitTaskContinuation(continuationAction, true); } if (taskContinuation != null) { if (!AddTaskContinuation(taskContinuation, false)) { //当线程池中的线程完成任务后该线程 会用taskContinuation.Run()中的SynchronizationContext同步上下文运行异步方法中剩下代码。//由于控制台没有显示的SynchronizationContext(控制台有默认的SynchronizationContext),
// 默认的SynchronizationContext没有转移控执行权的功能,所以控制台会继续使用多线程执行剩下异步方法中的代码。默认的SynchronizationContext内部有计数器来保证剩余的异步方法只执行一次。 // 但是ui线程有SynchronizationContext同步上下文DispachuerSynchronizationContext,该同步上下文会让会执行权转移到回到主线程,让主线程执行剩下的异步方法中的代码,如果主线程中调用方法中使用task.wait()或者task.result , taskContinuation.Run(this, false); } } else if (!AddTaskContinuation(continuationAction, false)) { //continueOnCapturedContext=false 执行这一步 AwaitTaskContinuation.UnsafeScheduleAction(continuationAction, this); } }
//那么就会形成死锁。
步骤10、跟踪到这里,终于看到真面目了,UnsafeQueueUserWorkItemInternal()方法向线程池的操作请求队列添加一个工作项以及其他数据项目,然后方法就会立即返回( 回到最开始的调用位置)。我知道还可以继续跟着,但我觉得跟踪到这边已经够了。
internal static void UnsafeScheduleAction(Action action, Task task) { AwaitTaskContinuation awaitTaskContinuation = new AwaitTaskContinuation(action, false); TplEventSource log = TplEventSource.Log; if (log.IsEnabled() && task != null) { awaitTaskContinuation.m_continuationId = Task.NewId(); log.AwaitTaskContinuationScheduled((task.ExecutingTaskScheduler ?? TaskScheduler.Default).Id, task.Id, awaitTaskContinuation.m_continuationId); } ThreadPool.UnsafeQueueUserWorkItemInternal(awaitTaskContinuation, true); }
回到最开始的调用位置<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
然后执行return;将控制权还给主程序。
private void MoveNext() { int num = <>1__state; int length; try { TaskAwaiter<string> awaiter; if (num != 0) {5__1 = new HttpClient(); 5__2 = 5__1.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html"); awaiter = 5__2.GetAwaiter(); if (!awaiter.IsCompleted) { num = (<>1__state = 0); <>u__1 = awaiter; d__1 stateMachine = this; <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); return; } }
最终线程池中的某个线程会处理该工作项。
参考: https://www.cnblogs.com/lsxqw2004/p/4922374.html