【C# TAP 异步编程】三、async\await的运作机理详解
阅读原文时间:2023年07月09日阅读:3

【原创】 本文只是个人笔记,很多错误,欢迎指出。

环境: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中默认使用异步来处理请求。

**
异步方法看起来像这样:**

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 和 Task。
特殊方法(如"Task.Yield")返回YieldAwaitable可等待对象,并且 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、Task 或 void。在几乎所有情况下,您都希望返回 Task 或 Task,并且仅在必要时返回 void。

为什么要返回 Task 或 Task?因为它们是可以等待的,而虚空不是。因此,如果您有一个异步方法返回 Task 或 Task,则可以将结果传递给等待。使用void方法,您无需等待任何东西。

当您具有异步事件处理程序时,必须返回 void。

返回 Task 或 void 的异步方法没有返回值。返回 Task 的异步方法必须返回 T 类型的值:

public async Task CalculateAnswer()
{
await Task.Delay(100); // (Probably should be longer…)

// Return a type of "int", not "Task"
return 42;
}

这有点奇怪,但这种设计背后有很好的理由

问:为什么我不能写这个:

// Hypothetical syntax
public async int GetValue()
{
await TaskEx.Delay(100);
return 13; // Return type is "int"
}

考虑一下:方法签名对调用方的外观如何?返回值的类型为是可等待的对象,那么我们对异步方法调用时候可以给该方法或对象标注成await。如果其他类型我们将很难判断该方法是否可以标注成await。

在概述中,我提到,当您等待内置的可等待对象时,等待对象将捕获当前的"上下文",然后将其应用于异步方法的其余部分。这个"背景"到底是什么?

简单的答案:

  1. 如果你位于 UI 线程上,则它是 UI 上下文。
  2. 如果您正在响应 ASP.NET 请求,则这是一个 ASP.NET 请求上下文。
  3. 否则,它通常是线程池上下文。

复杂的答案:

  1. 如果 SynchronizationContext.Current 不为 null,则它是当前的 SyncContext。(UI 和 ASP.NET 请求上下文是同步上下文上下文)。
  2. 否则,它是当前的 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 GetFirstToRespondAsync()
{
// Call two web services; take the first response.
Task[] tasks = new[] { WebService1Async(), WebService2Async() };

// Await for the first one to respond.
Task firstTask = await Task.WhenAny(tasks);

// Return the result.
return await firstTask;
}

通过使用并发组合(Task.WhenAll 或 Task.WhenAny),可以执行简单的并发操作。您还可以将这些方法与 Task.Run 一起使用,以进行简单的并行计算。但是,这不能替代任务并行库 - 任何高级 CPU 密集型并行操作都应使用 TPL 完成。

原文:异步和等待 (stephencleary.com)

异步的使用心得

当你省略await/async时候异常的报错方式也会不一样。
所以你当你理解异步原理后,还是建议不要省略await/async。

Task 无疑是一个现代 OVERLAPPED 抽象。它表示一些工作,通常是 I/O 密集型的,可以由 CPU 之外的某些工作完成,并且当该工作完成时,任务将收到通知。
事件处理程序是从同步角度设计的,这就是为什么将它们与异步一起使用是很尴尬的原因。

通过分析一段代码运行来了解async\await的运作机理(本案例是控制台应用程序)

namespace MyTask;
class Program
{
public static void Main(string[] args)
{
Task baconTask = GetUrlContentLengthAsync(3);
baconTask.ContinueWith(t => Console.WriteLine(t.Result));
Console.Read();
}
static async Task 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 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代码我们可以分析出整个代码运行过程 如下:

关系图中的数字对应于以下步骤,在调用方法调用异步方法时启动。

  1. Main调用方法调用并等待 GetUrlContentLengthAsync 异步方法。

  2. GetUrlContentLengthAsync 可创建 HttpClient 实例并调用 GetStringAsync 异步方法以下载网站内容作为字符串。

  3. GetStringAsync 中发生了某种情况,该情况挂起了它的进程。 可能必须等待网站下载或一些其他阻止活动。 为避免阻止资源,GetStringAsync 会将控制权出让给其调用方 GetUrlContentLengthAsync

    GetStringAsync 返回 Task,其中 TResult 为字符串,并且 GetUrlContentLengthAsync 将任务分配给 getStringTask 变量。 该任务表示调用 GetStringAsync 的正在进行的进程,其中承诺当工作完成时产生实际字符串值。

  4. 由于尚未等待 getStringTask,因此,GetUrlContentLengthAsync 可以继续执行不依赖于 GetStringAsync 得出的最终结果的其他工作。 该任务由对同步方法 DoIndependentWork 的调用表示。

  5. DoIndependentWork 是完成其工作并返回其调用方的同步方法。

  6. GetUrlContentLengthAsync 已运行完毕,可以不受 getStringTask 的结果影响。 接下来,GetUrlContentLengthAsync 需要计算并返回已下载的字符串的长度,但该方法只有在获得字符串的情况下才能计算该值。

    因此,GetUrlContentLengthAsync 使用一个 await 运算符来挂起其进度,并把控制权交给调用 GetUrlContentLengthAsync 的方法。 GetUrlContentLengthAsyncTask<int> 返回给调用方。 该任务表示对产生下载字符串长度的整数结果的一个承诺。

    备注

    如果 GetStringAsync(因此 getStringTask)在 GetUrlContentLengthAsync 等待前完成,则控制会保留在 GetUrlContentLengthAsync 中。 如果异步调用过程 getStringTask 已完成,并且 GetUrlContentLengthAsync 不必等待最终结果,则挂起然后返回到 GetUrlContentLengthAsync 将造成成本浪费。

    在调用方法中,处理模式会继续。 在等待结果前,调用方可以开展不依赖于 GetUrlContentLengthAsync 结果的其他工作,否则就需等待片刻。 调用方法等待 GetUrlContentLengthAsync,而 GetUrlContentLengthAsync 等待 GetStringAsync

  7. GetStringAsync 完成并生成一个字符串结果。 字符串结果不是通过按你预期的方式调用 GetStringAsync 所返回的。 (记住,该方法已返回步骤 3 中的一个任务)。相反,字符串结果存储在表示 getStringTask 方法完成的任务中。 await 运算符从 getStringTask 中检索结果。   赋值语句将检索到的结果赋给 contents

  8. GetUrlContentLengthAsync 具有字符串结果时,该方法可以计算字符串长度。 然后,GetUrlContentLengthAsync 工作也将完成,并且等待事件处理程序可继续使用。 在此主题结尾处的完整示例中,可确认事件处理程序检索并打印长度结果的值。 如果你不熟悉异步编程,请花 1 分钟时间考虑同步行为和异步行为之间的差异。 当其工作完成时(第 5 步)会返回一个同步方法,但当其工作挂起时(第 3 步和第 6 步),异步方法会返回一个任务值。 在异步方法最终完成其工作时,任务会标记为已完成,而结果(如果有)将存储在任务中。

以上内容来自:https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/task-asynchronous-programming-model

反编译后我们可以看到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 GetStringAsync(string? requestUri)返回值Task 中的TResult。

此例子 中httpClient.GetStringAsync() 返回值是Task

所以等待器 TaskAwaiter的返回值是 string(Task中的string)。

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__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(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<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 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