【C# TAP 异步编程】三、async\await的运作机理详解
【原创】 本文只是个人笔记,很多错误,欢迎指出。
环境:vs2022 .net6.0 C#10
参考:https://blog.csdn.net/brook_shi/article/details/50803957
Await 就像一个一元运算符:它接受一个参数,一个可等待的("awaitable"是一个异步操作)
使用场景:1、首次显示页面/表单时,需要将其同步初始化为一种"正在加载"状态,然后启动异步操作以检索所需的数据。稍后,当数据到达时,更新现有页面/表单以显示处于"已加载"状态的数据。同样适用与wpf开发。
异步编程
异步编程就是使用future模式(又称promise)或者回调机制来实现(Non-blocking on waiting)。
如果使用回调或事件来实现(容易callback hell),不仅编写这样的代码不直观,很快就容易把代码搞得一团糟。不过在.NET 4.5(C# 5)中引入的async/await关键字(在.NET 4.0中通过添加Microsoft.Bcl.Async包也可以使用),让编写异步代码变得容易和优雅。通过使用async/await关键字,可以像写同步代码那样编写异步代码,所有的回调和事件处理都交给编译器和运行时帮你处理了。
使用异步编程有两个好处:不阻塞主线程(比如UI线程),提高服务端应用的吞吐量。所以微软推荐ASP.NET中默认使用异步来处理请求。
介绍关键Async 和 Await
异步方法看起来像这样:
public async Task DoSomethingAsync() { // In the Real World, we would actually do something... // For this example, we're just going to (asynchronously) wait 100ms. await Task.Delay(100); }
“async”关键字启用该方法中的“await”关键字,并让编译器改变该方法的处理方式,生成一个异步状态机类,将方法内部代码剪切到状态机类的内部,这就是async关键字所做的一切!它不会在线程池线程上运行此方法。async关键字只启用await关键字(并管理方法结果)。
异步方法的开始就像其他方法一样执行。也就是说,它同步运行,直到遇到“await”(或抛出异常)。
await关键字是可以实现异步的地方。Await就像一个一元操作符:它只接受一个参数,一个awaitable(“awaitable”是一个异步操作)。Await检查Await,看看它是否已经完成;如果awaitable已经完成,那么该方法将继续运行(同步运行,就像常规方法一样)。
如果“await”看到awaitable还没有完成,那么它将采取异步操作。它告诉awaitable在完成时运行该方法的剩余部分,然后从async方法返回。
稍后,当awaitable完成时,它将执行async方法的其余部分。如果你正在等待一个内置的awaitable(比如一个任务),那么async方法的其余部分将在“await”返回之前捕获的“上下文”上执行。
我喜欢将“等待”视为“异步等待”。也就是说,async方法会暂停直到awaitable完成(所以它会等待),但实际的线程没有被阻塞(所以它是异步的)。
等待者模式
Await/Async 异步使模式使用的是等待者模式。等待者模式要求等待者公开IsCompleted属性,GetResult
方法和OnCompleted
方法(可选地带有UnsafeOnCompleted
方法,unsafe表示不使用可执行上下 回导致漏洞)。
Awaitable-可等待类型
正如我所提到的,"await"采用单个参数 - 一个"awaitable" - 这是一个异步操作。.NET 框架中已经有两种常见的可等待类型:Task<T> 和 Task。
特殊方法(如"Task.Yield")返回YieldAwaitabl
e可等待对象,并且 WinRT 运行时(在 Windows 8 中提供)具有非托管的可等待类型。
您还可以创建自己的可等待类型(通常出于性能原因),或使用扩展方法使不可等待的类型可等待。
关于 awaitables 的一个要点是:它是可等待的类型,而不是返回该类型的方法。换句话说,您可以等待返回 Task ...因为该方法返回 Task,而不是因为它是异步的。因此,您也可以等待返回 Task 的非异步方法的结果:
public async Task NewStuffAsync() { // 异步方法 await ... } public Task MyOldTaskParallelLibraryCode() { // 不是异步方法 ... } public async Task ComposeAsync() { // We can await Tasks, regardless of where they come from. await NewStuffAsync(); await MyOldTaskParallelLibraryCode(); }
返回类型
异步方法可以返回 Task<T>、Task 或 void。在几乎所有情况下,您都希望返回 Task<T> 或 Task,并且仅在必要时返回 void。
为什么要返回 Task<T> 或 Task?因为它们是可以等待的,而虚空不是。因此,如果您有一个异步方法返回 Task<T> 或 Task,则可以将结果传递给等待。使用void方法,您无需等待任何东西。
当您具有异步事件处理程序时,必须返回 void。
返回值
返回 Task 或 void 的异步方法没有返回值。返回 Task<T> 的异步方法必须返回 T 类型的值:
public async Task<int> CalculateAnswer() { await Task.Delay(100); // (Probably should be longer...) // Return a type of "int", not "Task<int>" return 42; }
这有点奇怪,但这种设计背后有很好的理由。
问:为什么我不能写这个:
// Hypothetical syntax public async int GetValue() { await TaskEx.Delay(100); return 13; // Return type is "int" }
考虑一下:方法签名对调用方的外观如何?返回值的类型为是可等待的对象,那么我们对异步方法调用时候可以给该方法或对象标注成await。如果其他类型我们将很难判断该方法是否可以标注成await。
上下文
在概述中,我提到,当您等待内置的可等待对象时,等待对象将捕获当前的"上下文",然后将其应用于异步方法的其余部分。这个"背景"到底是什么?
简单的答案:
- 如果你位于 UI 线程上,则它是 UI 上下文。
- 如果您正在响应 ASP.NET 请求,则这是一个 ASP.NET 请求上下文。
- 否则,它通常是线程池上下文。
复杂的答案:
- 如果 SynchronizationContext.Current 不为 null,则它是当前的 SyncContext。(UI 和 ASP.NET 请求上下文是同步上下文上下文)。
- 否则,它是当前的 TaskScheduler(TaskScheduler.Default 是线程池上下文)。
这在现实世界中意味着什么?首先,捕获(和还原)UI/ASP.NET 上下文是透明完成的:
// WinForms example (it works exactly the same for WPF). private async void DownloadFileButton_Click(object sender, EventArgs e) { // Since we asynchronously wait, the UI thread is not blocked by the file download. await DownloadFileAsync(fileNameTextBox.Text); // Since we resume on the UI context, we can directly access UI elements. resultTextBox.Text = "File downloaded!"; } // ASP.NET example protected async void MyButton_Click(object sender, EventArgs e) { // Since we asynchronously wait, the ASP.NET thread is not blocked by the file download. // This allows the thread to handle other requests while we're waiting. await DownloadFileAsync(...); // Since we resume on the ASP.NET context, we can access the current request. // We may actually be on another *thread*, but we have the same ASP.NET request context. Response.Write("File downloaded!"); }
这对于事件处理程序非常有用,但事实证明,它不是您希望大多数其他代码(实际上,您将要编写的大多数异步代码)所需要的。
避免上下文
大多数情况下,您不需要同步回"主要"上下文。大多数异步方法在设计时都会考虑组合:它们等待其他操作,每个操作本身都表示一个异步操作(可以由其他操作组成)。在这种情况下,您希望通过调用 ConfigureAwait 并传递 false 来告诉等待者不要捕获当前上下文,例如:
private async Task DownloadFileAsync(string fileName) { // Use HttpClient or whatever to download the file contents. var fileContents = await DownloadFileContentsAsync(fileName).ConfigureAwait(false); // Note that because of the ConfigureAwait(false), we are not on the original context here. // Instead, we're running on the thread pool. // Write the file contents out to a disk file. await WriteToDiskAsync(fileName, fileContents).ConfigureAwait(false); // The second call to ConfigureAwait(false) is not *required*, but it is Good Practice. } // WinForms example (it works exactly the same for WPF). private async void DownloadFileButton_Click(object sender, EventArgs e) { // Since we asynchronously wait, the UI thread is not blocked by the file download. await DownloadFileAsync(fileNameTextBox.Text); // Since we resume on the UI context, we can directly access UI elements. resultTextBox.Text = "File downloaded!"; }
此示例需要注意的重要一点是,异步方法调用的每个"级别"都有自己的上下文。DownloadFileButton_Click在 UI 上下文中启动,名为 DownloadFileAsync。DownloadFileAsync 也在 UI 上下文中启动,但随后通过调用 ConfigureAwait(false) 退出了其上下文。下载文件异步的其余部分在线程池上下文中运行。但是,当 DownloadFileAsync 完成并DownloadFileButton_Click恢复时,它会在 UI 上下文中恢复。
一个好的经验法则是使用 ConfigureAwait(false),除非您知道确实需要上下文。
异步组合
到目前为止,我们只考虑了串行组合:异步方法一次等待一个操作。也可以启动多个操作并等待其中一个(或所有)完成。您可以通过启动操作来执行此操作,但直到稍后才等待它们:
public async Task DoOperationsConcurrentlyAsync() { Task[] tasks = new Task[3]; tasks[0] = DoOperation0Async(); tasks[1] = DoOperation1Async(); tasks[2] = DoOperation2Async(); // At this point, all three tasks are running at the same time. // Now, we await them all. await Task.WhenAll(tasks); } public async Task<int> GetFirstToRespondAsync() { // Call two web services; take the first response. Task<int>[] tasks = new[] { WebService1Async(), WebService2Async() }; // Await for the first one to respond. Task<int> firstTask = await Task.WhenAny(tasks); // Return the result. return await firstTask; }
通过使用并发组合(Task.WhenAll 或 Task.WhenAny),可以执行简单的并发操作。您还可以将这些方法与 Task.Run 一起使用,以进行简单的并行计算。但是,这不能替代任务并行库 - 任何高级 CPU 密集型并行操作都应使用 TPL 完成。
异步的使用心得
当你省略await/async时候异常的报错方式也会不一样。
所以你当你理解异步原理后,还是建议不要省略await/async。
Task 无疑是一个现代 OVERLAPPED 抽象。它表示一些工作,通常是 I/O 密集型的,可以由 CPU 之外的某些工作完成,并且当该工作完成时,任务将收到通知。
事件处理程序是从同步角度设计的,这就是为什么将它们与异步一起使用是很尴尬的原因。
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 = <getContent>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 = <getContent>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 Action<Task<int>> <>9__0_0; internal void <Main>b__0_0(Task<int> t) { Console.WriteLine(t.Result); } } [CompilerGenerated] private sealed class <GetUrlContentLengthAsync>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 <httpClient>5__1; [System.Runtime.CompilerServices.Nullable(new byte[] { 0, 1 })] private Task<string> <getContent>5__2; [System.Runtime.CompilerServices.Nullable(0)] private string <content>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) { <httpClient>5__1 = new HttpClient(); <getContent>5__2 = <httpClient>5__1.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html"); awaiter = <getContent>5__2.GetAwaiter(); if (!awaiter.IsCompleted) { num = (<>1__state = 0); <>u__1 = awaiter; <GetUrlContentLengthAsync>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(); <content>5__3 = <>s__4; <>s__4 = null; DoInDependentWork(); length = <content>5__3.Length; } catch (Exception exception) { <>1__state = -2; <httpClient>5__1 = null; <getContent>5__2 = null; <content>5__3 = null; <>t__builder.SetException(exception); return; } <>1__state = -2; <httpClient>5__1 = null; <getContent>5__2 = null; <content>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<Task<int>>(<>c.<>9.<Main>b__0_0))); Console.Read(); } [AsyncStateMachine(typeof(<GetUrlContentLengthAsync>d__1))] [DebuggerStepThrough] private static Task<int> GetUrlContentLengthAsync(int slices) { <GetUrlContentLengthAsync>d__1 stateMachine = new <GetUrlContentLengthAsync>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>,其中TResult
为字符串,并且GetUrlContentLengthAsync
将任务分配给getStringTask
变量。 该任务表示调用GetStringAsync
的正在进行的进程,其中承诺当工作完成时产生实际字符串值。 -
由于尚未等待
getStringTask
,因此,GetUrlContentLengthAsync
可以继续执行不依赖于GetStringAsync
得出的最终结果的其他工作。 该任务由对同步方法DoIndependentWork
的调用表示。 -
DoIndependentWork
是完成其工作并返回其调用方的同步方法。 -
GetUrlContentLengthAsync
已运行完毕,可以不受getStringTask
的结果影响。 接下来,GetUrlContentLengthAsync
需要计算并返回已下载的字符串的长度,但该方法只有在获得字符串的情况下才能计算该值。因此,
GetUrlContentLengthAsync
使用一个 await 运算符来挂起其进度,并把控制权交给调用GetUrlContentLengthAsync
的方法。GetUrlContentLengthAsync
将Task<int>
返回给调用方。 该任务表示对产生下载字符串长度的整数结果的一个承诺。备注
如果
GetStringAsync
(因此getStringTask
)在GetUrlContentLengthAsync
等待前完成,则控制会保留在GetUrlContentLengthAsync
中。 如果异步调用过程getStringTask
已完成,并且GetUrlContentLengthAsync
不必等待最终结果,则挂起然后返回到GetUrlContentLengthAsync
将造成成本浪费。在调用方法中,处理模式会继续。 在等待结果前,调用方可以开展不依赖于
GetUrlContentLengthAsync
结果的其他工作,否则就需等待片刻。 调用方法等待GetUrlContentLengthAsync
,而GetUrlContentLengthAsync
等待GetStringAsync
。 -
GetStringAsync
完成并生成一个字符串结果。 字符串结果不是通过按你预期的方式调用GetStringAsync
所返回的。 (记住,该方法已返回步骤 3 中的一个任务)。相反,字符串结果存储在表示getStringTask
方法完成的任务中。 await 运算符从getStringTask
中检索结果。 赋值语句将检索到的结果赋给contents
。 -
当
GetUrlContentLengthAsync
具有字符串结果时,该方法可以计算字符串长度。 然后,GetUrlContentLengthAsync
工作也将完成,并且等待事件处理程序可继续使用。 在此主题结尾处的完整示例中,可确认事件处理程序检索并打印长度结果的值。 如果你不熟悉异步编程,请花 1 分钟时间考虑同步行为和异步行为之间的差异。 当其工作完成时(第 5 步)会返回一个同步方法,但当其工作挂起时(第 3 步和第 6 步),异步方法会返回一个任务值。 在异步方法最终完成其工作时,任务会标记为已完成,而结果(如果有)将存储在任务中。
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<TResult> GetStringAsync(string? requestUri)返回值Task<TResult> 中的TResult。
此例子 中httpClient.GetStringAsync() 返回值是Task<string>。
所以等待器 TaskAwaiter的返回值是 string(Task<string>中的string)。
ConfiguredTaskAwaiter等待器
通过第 以下代码了解 等二种等待器类型
await httpClient.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html").ConfigureAwait(false);生成ConfiguredTaskAwaitable<string>.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 = <getContent>5__2.GetAwaiter(); 将状态机更改成0;然后运行<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
步骤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<TAwaiter, TaskAwaiter>(ref awaiter).m_task, box, true); return; } //第二种等待器 根据初始化设置决定要不要获取同步上下文 if (default(TAwaiter) != null && awaiter is IConfiguredTaskAwaiter) { ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter reference = ref Unsafe.As<TAwaiter, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter>(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<Tresult>.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) { <httpClient>5__1 = new HttpClient(); <getContent>5__2 = <httpClient>5__1.GetStringAsync("https://www.cnblogs.com/majiang/p/7899202.html"); awaiter = <getContent>5__2.GetAwaiter(); if (!awaiter.IsCompleted) { num = (<>1__state = 0); <>u__1 = awaiter; <GetUrlContentLengthAsync>d__1 stateMachine = this; <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); return; } }
最终线程池中的某个线程会处理该工作项。
参考: https://www.cnblogs.com/lsxqw2004/p/4922374.html