理解C#中的 async await

xiaoxiaotank發表於2021-01-21

前言

一個老掉牙的話題,園子裡的相關優秀文章已經有很多了,我寫這篇文章完全是想以自己的思維方式來談一談自己的理解。(PS:文中涉及到了大量反編譯原始碼,需要靜下心來細細品味)

從簡單開始

為了更容易理解這個問題,我們舉一個簡單的例子:用非同步的方式在控制檯上分兩步輸出“Hello World!”,我這邊使用的是Framework 4.5.2

class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("Let's Go!");
    
        await TestAsync();
        
        Console.Write(" World!");
    }

    static Task TestAsync()
    {
        return Task.Run(() =>
        {
            Console.Write("Hello");
        });
    }
}

探究反編譯後的原始碼

接下來我們使用 .NET reflector (也可使用 dnSpy 等) 反編譯一下程式集,然後一步一步來探究 async await 內部的奧祕。

Main方法

[DebuggerStepThrough]
private static void <Main>(string[] args)
{
    Main(args).GetAwaiter().GetResult();
}

[AsyncStateMachine(typeof(<Main>d__0)), DebuggerStepThrough]
private static Task Main(string[] args)
{
    <Main>d__0 stateMachine = new <Main>d__0 
    {
        <>t__builder = AsyncTaskMethodBuilder.Create(),
        args = args,
        <>1__state = -1
    };
    stateMachine.<>t__builder.Start<<Main>d__0>(ref stateMachine);
    return stateMachine.<>t__builder.Task;
}

// 實現了 IAsyncStateMachine 介面
[CompilerGenerated]
private sealed class <Main>d__0 : IAsyncStateMachine
{
    // Fields
    public int <>1__state;
    public AsyncTaskMethodBuilder <>t__builder;
    public string[] args;
    private TaskAwaiter <>u__1;

    // Methods
    private void MoveNext() { }
    [DebuggerHidden]
    private void SetStateMachine(IAsyncStateMachine stateMachine) { }
}

臥槽!竟然有兩個 Main 方法:一個同步、一個非同步。原來,雖然我們寫程式碼時為了在 Main 方法中方便非同步等待,將 void Main 改寫成了async Task Main,但是實際上程式入口仍是我們熟悉的那個 void Main。

另外,我們可以看到非同步 Main 方法被標註了AsyncStateMachine特性,這是因為在我們的原始碼中,該方法帶有修飾符async,表示該方法是一個非同步方法。

好,我們先看一下非同步Main方法內部實現,它主要做了三件事:

  1. 首先,建立了一個型別為<Main>d__0的狀態機 stateMachine,並初始化了公共變數 <>t__builder、args、<>1__state = -1
    • <>t__builder:負責非同步相關的操作,是實現非同步 Main 方法非同步的核心
    • <>1__state:狀態機的當前狀態
  2. 然後,呼叫Start方法,藉助 stateMachine, 來執行我們在非同步 Main 方法中寫的程式碼
  3. 最後,將指示非同步 Main 方法執行狀態的Task物件返回出去

Start

首先,我們先來看一下Start的內部實現

// 所屬結構體:AsyncTaskMethodBuilder

[SecuritySafeCritical, DebuggerStepThrough, __DynamicallyInvokable]
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine: IAsyncStateMachine
{
    if (((TStateMachine) stateMachine) == null)
    {
        throw new ArgumentNullException("stateMachine");
    }
    ExecutionContextSwitcher ecsw = new ExecutionContextSwitcher();
    RuntimeHelpers.PrepareConstrainedRegions();
    try
    {
        ExecutionContext.EstablishCopyOnWriteScope(ref ecsw);
        // 狀態機狀態流轉
        stateMachine.MoveNext();
    }
    finally
    {
        ecsw.Undo();
    }
}

我猜,你只能看懂stateMachine.MoveNext(),對不對?好,那我們就來看看這個狀態機類<Main>d__0,並且著重看它的方法MoveNext

MoveNext

[CompilerGenerated]
private sealed class <Main>d__0 : IAsyncStateMachine
{
    // Fields
    public int <>1__state;
    public AsyncTaskMethodBuilder <>t__builder;
    public string[] args;
    private TaskAwaiter <>u__1;

    // Methods
    private void MoveNext()
    {
        // 在 Main 方法中,我們初始化 <>1__state = -1,所以此時 num = -1
        int num = this.<>1__state;
        try
        {
            TaskAwaiter awaiter;
            if (num != 0)
            {
                Console.WriteLine("Let's Go!");
                // 呼叫 TestAsync(),獲取 awaiter,用於後續監控 TestAsync() 執行狀態
                awaiter = Program.TestAsync().GetAwaiter();
                
                // 一般來說,非同步任務不會很快就完成,所以大多數情況下都會進入該分支
                if (!awaiter.IsCompleted)
                {
                    // 狀態機狀態從 -1 流轉為 0
                    this.<>1__state = num = 0;
                    this.<>u__1 = awaiter;
                    Program.<Main>d__0 stateMachine = this;
                    // 配置  TestAsync() 完成後的延續
                    this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<Main>d__0>(ref awaiter, ref stateMachine);
                    return;
                }
            }
            else
            {
                awaiter = this.<>u__1;
                this.<>u__1 = new TaskAwaiter();
                this.<>1__state = num = -1;
            }
            awaiter.GetResult();
            Console.Write(" World!");
        }
        catch (Exception exception)
        {
            this.<>1__state = -2;
            this.<>t__builder.SetException(exception);
            return;
        }
        this.<>1__state = -2;
        this.<>t__builder.SetResult();
    }

    [DebuggerHidden]
    private void SetStateMachine(IAsyncStateMachine stateMachine)
    {
    }
}

先簡單理一下內部邏輯:

  1. 設定變數 num = -1,此時 num != 0,則會進入第一個if語句,
  2. 首先,執行Console.WriteLine("Let's Go!")
  3. 然後,呼叫非同步方法TestAsyncTestAsync方法會在另一個執行緒池執行緒中執行,並獲取指示該方法執行狀態的 awaiter
  4. 如果此時TestAsync方法已執行完畢,則像沒有非同步一般:
    1. 繼續執行接下來的Console.Write(" World!")
    2. 最後設定 <>1__state = -2,並設定非同步 Main 方法的返回結果
  5. 如果此時TestAsync方法未執行完畢,則:
    1. 設定 <>1__state = num = 0
    2. 呼叫AwaitUnsafeOnCompleted方法,用於配置當TestAsync方法完成時的延續,即Console.Write(" World!")
    3. 返回指示非同步 Main 方法執行狀態的 Task 物件,由於同步 Main 方法中通過使用GetResult()同步阻塞主執行緒等待任務結束,所以不會釋放主執行緒(廢話,如果釋放了程式就退出了)。不過對於其他子執行緒,一般會釋放該執行緒

大部分邏輯我們都可以很容易的理解,唯一需要深入研究的就是AwaitUnsafeOnCompleted,那我們接下來就看看它的內部實現

AwaitUnsafeOnCompleted

// 所屬結構體:AsyncTaskMethodBuilder

[__DynamicallyInvokable]
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter: ICriticalNotifyCompletion where TStateMachine: IAsyncStateMachine
{
    this.m_builder.AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref awaiter, ref stateMachine);
}

// 所屬結構體:AsyncTaskMethodBuilder<TResult>

[SecuritySafeCritical, __DynamicallyInvokable]
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter: ICriticalNotifyCompletion where TStateMachine: IAsyncStateMachine
{
    try
    {
        // 用於流轉狀態機狀態的 runner
        AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize = null;
        Action completionAction = this.m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn ? this.Task : null, ref runnerToInitialize);
        if (this.m_coreState.m_stateMachine == null)
        {
            // 此處構建指示非同步 Main 方法執行狀態的 Task 物件
            Task<TResult> builtTask = this.Task;
            this.m_coreState.PostBoxInitialization((TStateMachine) stateMachine, runnerToInitialize, builtTask);
        }
        awaiter.UnsafeOnCompleted(completionAction);
    }
    catch (Exception exception)
    {
        AsyncMethodBuilderCore.ThrowAsync(exception, null);
    }
}

我們們一步一步來,先看一下GetCompletionAction的實現:

// 所屬結構體:AsyncMethodBuilderCore

[SecuritySafeCritical]
internal Action GetCompletionAction(Task taskForTracing, ref MoveNextRunner runnerToInitialize)
{
    Action defaultContextAction;
    MoveNextRunner runner;
    Debugger.NotifyOfCrossThreadDependency();
    // 
    ExecutionContext context = ExecutionContext.FastCapture();
    if ((context != null) && context.IsPreAllocatedDefault)
    {
        defaultContextAction = this.m_defaultContextAction;
        if (defaultContextAction != null)
        {
            return defaultContextAction;
        }
        
        // 構建 runner
        runner = new MoveNextRunner(context, this.m_stateMachine);
        // 返回值
        defaultContextAction = new Action(runner.Run);
        if (taskForTracing != null)
        {
            this.m_defaultContextAction = defaultContextAction = this.OutputAsyncCausalityEvents(taskForTracing, defaultContextAction);
        }
        else
        {
            this.m_defaultContextAction = defaultContextAction;
        }
    }
    else
    {
        runner = new MoveNextRunner(context, this.m_stateMachine);
        defaultContextAction = new Action(runner.Run);
        if (taskForTracing != null)
        {
            defaultContextAction = this.OutputAsyncCausalityEvents(taskForTracing, defaultContextAction);
        }
    }
    if (this.m_stateMachine == null)
    {
        runnerToInitialize = runner;
    }
    return defaultContextAction;
}

發現一個熟悉的傢伙——ExecutionContext,它是用來給我們們延續方法(即Console.Write(" World!");)提供執行環境的,注意這裡用的是FastCapture(),該內部方法並未捕獲SynchronizationContext,因為不需要流動它。什麼?你說你不認識它?大眼瞪小眼?那你應該好好看看《理解C#中的ExecutionContext vs SynchronizationContext》

接著來到new MoveNextRunner(context, this.m_stateMachine),這裡初始化了 runner,我們看看建構函式中做了什麼:

[SecurityCritical]
internal MoveNextRunner(ExecutionContext context, IAsyncStateMachine stateMachine)
{
    // 將 ExecutionContext 儲存了下來
    this.m_context = context;
    
    // 將 stateMachine 儲存了下來(不過此時為 null)
    this.m_stateMachine = stateMachine;
}

往下來到defaultContextAction = new Action(runner.Run),你可以發現,最終我們們返回的就是這個 defaultContextAction ,所以這個runner.Run至關重要,不過彆著急,我們等用到它的時候我們再來看其內部實現。

最後,回到AwaitUnsafeOnCompleted方法,繼續往下走。構建指示非同步 Main 方法執行狀態的 Task 物件,設定當前的狀態機後,來到awaiter.UnsafeOnCompleted(completionAction);,要記住,入參 completionAction 就是剛才返回的runner.Run

// 所屬結構體:TaskAwaiter

[SecurityCritical, __DynamicallyInvokable]
public void UnsafeOnCompleted(Action continuation)
{
    OnCompletedInternal(this.m_task, continuation, true, false);
}

[MethodImpl(MethodImplOptions.NoInlining), SecurityCritical]
internal static void OnCompletedInternal(Task task, Action continuation, bool continueOnCapturedContext, bool flowExecutionContext)
{
    if (continuation == null)
    {
        throw new ArgumentNullException("continuation");
    }
    StackCrawlMark lookForMyCaller = StackCrawlMark.LookForMyCaller;
    if (TplEtwProvider.Log.IsEnabled() || Task.s_asyncDebuggingEnabled)
    {
        continuation = OutputWaitEtwEvents(task, continuation);
    }
    
    // 配置延續方法
    task.SetContinuationForAwait(continuation, continueOnCapturedContext, flowExecutionContext, ref lookForMyCaller);
}

直接來到程式碼最後一行,看到延續方法的配置

// 所屬類:Task

[SecurityCritical]
internal void SetContinuationForAwait(Action continuationAction, bool continueOnCapturedContext, bool flowExecutionContext, ref StackCrawlMark stackMark)
{
    TaskContinuation tc = null;
    if (continueOnCapturedContext)
    {
        // 這裡我們用的是不進行流動的 SynchronizationContext
        SynchronizationContext currentNoFlow = SynchronizationContext.CurrentNoFlow;
        // 像 Winform、WPF 這種框架,實現了自定義的 SynchronizationContext,
        // 所以在 Winform、WPF 的 UI執行緒中進行非同步等待時,一般 currentNoFlow 不會為 null
        if ((currentNoFlow != null) && (currentNoFlow.GetType() != typeof(SynchronizationContext)))
        {
            // 如果有 currentNoFlow,那麼我就用它來執行延續方法
            tc = new SynchronizationContextAwaitTaskContinuation(currentNoFlow, continuationAction, flowExecutionContext, ref stackMark);
        }
        else
        {
            TaskScheduler internalCurrent = TaskScheduler.InternalCurrent;
            if ((internalCurrent != null) && (internalCurrent != TaskScheduler.Default))
            {
                tc = new TaskSchedulerAwaitTaskContinuation(internalCurrent, continuationAction, flowExecutionContext, ref stackMark);
            }
        }
    }
    if ((tc == null) & flowExecutionContext)
    {
        tc = new AwaitTaskContinuation(continuationAction, true, ref stackMark);
    }
    if (tc != null)
    {
        if (!this.AddTaskContinuation(tc, false))
        {
            tc.Run(this, false);
        }
    }
    // 這裡會將 continuationAction 設定為 awaiter 中 task 物件的延續方法,所以當 TestAsync() 完成時,就會執行 runner.Run
    else if (!this.AddTaskContinuation(continuationAction, false))
    {
        AwaitTaskContinuation.UnsafeScheduleAction(continuationAction, this);
    }
}

對於我們的示例來說,既沒有自定義 SynchronizationContext,也沒有自定義 TaskScheduler,所以會直接來到最後一個else if (...),重點在於this.AddTaskContinuation(continuationAction, false),這個方法會將我們的延續方法新增到 Task 中,以便於當 TestAsync 方法執行完畢時,執行 runner.Run

runner.Run

好,是時候讓我們看看 runner.Run 的內部實現了:

[SecuritySafeCritical]
internal void Run()
{
    if (this.m_context != null)
    {
        try
        {
            // 我們並未給 s_invokeMoveNext 賦值,所以 callback == null
            ContextCallback callback = s_invokeMoveNext;
            if (callback == null)
            {
                // 將回撥設定為下方的 InvokeMoveNext 方法
                s_invokeMoveNext = callback = new
                ContextCallback(AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext);
            }
            ExecutionContext.Run(this.m_context, callback, this.m_stateMachine, true);
            return;
        }
        finally
        {
            this.m_context.Dispose();
        }
    }
    this.m_stateMachine.MoveNext();
}

[SecurityCritical]
private static void InvokeMoveNext(object stateMachine)
{
    ((IAsyncStateMachine) stateMachine).MoveNext();
}

來到ExecutionContext.Run(this.m_context, callback, this.m_stateMachine, true);,這裡的 callback 是InvokeMoveNext方法。所以,當TestAsync執行完畢後,就會執行延續方法 runner.Run,也就會執行stateMachine.MoveNext()促使狀態機繼續進行狀態流轉,這樣邏輯就打通了:

private void MoveNext()
{
    // num = 0
    int num = this.<>1__state;
    try
    {
        TaskAwaiter awaiter;
        if (num != 0)
        {
            Console.WriteLine("Let's Go!");
            awaiter = Program.TestAsync().GetAwaiter();

            if (!awaiter.IsCompleted)
            {
                this.<>1__state = num = 0;
                this.<>u__1 = awaiter;
                Program.<Main>d__0 stateMachine = this;
                this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<Main>d__0>(ref awaiter, ref stateMachine);
                return;
            }
        }
        else
        {
            awaiter = this.<>u__1;
            this.<>u__1 = new TaskAwaiter();
            // 狀態機狀態從 0 流轉到 -1
            this.<>1__state = num = -1;
        }
        
        // 結束對 TestAsync() 的等待
        awaiter.GetResult();
        // 執行延續方法
        Console.Write(" World!");
    }
    catch (Exception exception)
    {
        this.<>1__state = -2;
        this.<>t__builder.SetException(exception);
        return;
    }
    
    // 狀態機狀態從 -1 流轉到 -2
    this.<>1__state = -2;
    // 設定非同步 Main 方法最終返回結果
    this.<>t__builder.SetResult();
}

至此,整個非同步方法的執行就結束了,通過一張圖總結一下:

最後,我們看一下各個執行緒的狀態,看看和你的推理是否一致(如果有不清楚的聯絡我,我會通過文字補充):

多個 async await 巢狀

理解了async await的簡單使用,那你可曾想過,如果有多個 async await 巢狀,那會出現什麼情況呢?接下來就改造一下我們的例子,來研究研究:

static Task TestAsync()
{
    return Task.Run(async () =>
    {
        // 增加了這行
        await Task.Run(() =>
        {
            Console.Write("Say: ");
        });

        Console.Write("Hello");
    });
}

反編譯之後的程式碼,上面已經講解的我就不再重複貼了,主要看看TestAsync()就行了:

private static Task TestAsync() => 
    Task.Run(delegate {
        <>c.<<TestAsync>b__1_0>d stateMachine = new <>c.<<TestAsync>b__1_0>d {
            <>t__builder = AsyncTaskMethodBuilder.Create(),
            <>4__this = this,
            <>1__state = -1
        };
        stateMachine.<>t__builder.Start<<>c.<<TestAsync>b__1_0>d>(ref stateMachine);
        return stateMachine.<>t__builder.Task;
    });

哦!原來,async await 的巢狀也就是狀態機的巢狀,相信你通過上面的狀態機狀態流轉,也能夠梳理除真正的執行邏輯,那我們就只看一下執行緒狀態吧:

這也印證了我上面所說的:當子執行緒完成執行任務時,會被釋放,或回到執行緒池供其他執行緒使用。

多個 async await 在同一方法中順序執行

又可曾想過,如果有多個 async await 在同一方法中順序執行,又會是何種景象呢?同樣,先來個例子:

static async Task Main(string[] args)
{
    Console.WriteLine("Let's Go!");

    await Test1Async();

    await Test2Async();

    Console.Write(" World!");
}

static Task Test1Async()
{
    return Task.Run(() =>
    {
        Console.Write("Say: ");
    });
}

static Task Test2Async()
{
    return Task.Run(() =>
    {
        Console.Write("Hello");
    });
}

直接看狀態機:

[CompilerGenerated]
private sealed class <Main>d__0 : IAsyncStateMachine
{
	// Fields
	public int <>1__state;
	public AsyncTaskMethodBuilder <>t__builder;
	public string[] args;
	private TaskAwaiter <>u__1;

    // Methods
	private void MoveNext()
	{
		int num = this.<>1__state;
		try
		{
			TaskAwaiter awaiter;
			TaskAwaiter awaiter2;
			if (num != 0)
			{
				if (num == 1)
				{
					awaiter = this.<>u__1;
					this.<>u__1 = default(TaskAwaiter);
					this.<>1__state = -1;
					goto IL_D8;
				}
				Console.WriteLine("Let's Go!");
				awaiter2 = Program.Test1Async().GetAwaiter();
				if (!awaiter2.IsCompleted)
				{
					this.<>1__state = 0;
					this.<>u__1 = awaiter2;
					Program.<Main>d__0 <Main>d__ = this;
					this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<Main>d__0>(ref awaiter2, ref <Main>d__);
					return;
				}
			}
			else
			{
				awaiter2 = this.<>u__1;
				this.<>u__1 = default(TaskAwaiter);
				this.<>1__state = -1;
			}
			awaiter2.GetResult();
			
			// 待 Test1Async 完成後,繼續執行 Test2Async
			awaiter = Program.Test2Async().GetAwaiter();
			if (!awaiter.IsCompleted)
			{
				this.<>1__state = 1;
				this.<>u__1 = awaiter;
				Program.<Main>d__0 <Main>d__ = this;
				this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<Main>d__0>(ref awaiter, ref <Main>d__);
				return;
			}
			IL_D8:
			awaiter.GetResult();
			Console.Write(" World!");
		}
		catch (Exception exception)
		{
			this.<>1__state = -2;
			this.<>t__builder.SetException(exception);
			return;
		}
		this.<>1__state = -2;
		this.<>t__builder.SetResult();
	}

	[DebuggerHidden]
	private void SetStateMachine(IAsyncStateMachine stateMachine)
	{
	}
}

可見,就是一個狀態機狀態一直流轉就完事了。我們就看看執行緒狀態吧:

WPF中使用 async await

上面我們都是通過控制檯舉的例子,這是沒有任何SynchronizationContext的,但是WPF(Winform同理)就不同了,在UI執行緒中,它擁有屬於自己的DispatcherSynchronizationContext

private async void Button_Click(object sender, RoutedEventArgs e)
{
    // UI 執行緒會一直保持 Running 狀態,不會導致 UI 假死
    Show(Thread.CurrentThread);

    await TestAsync();

    Show(Thread.CurrentThread);
}

private Task TestAsync()
{
    return Task.Run(() =>
    {
        Show(Thread.CurrentThread);
    });
}

private static void Show(Thread thread)
{
    MessageBox.Show($"{nameof(thread.ManagedThreadId)}: {thread.ManagedThreadId}" +
        $"\n{nameof(thread.ThreadState)}: {thread.ThreadState}");
}

通過使用DispatcherSynchronizationContext執行延續方法,又回到了 UI 執行緒中

相關文章