More Effective C# 27.非同步工作使用 Async 方法 More Effective C# 27.非同步工作使用 Async 方法(Use Async methods for async work)

這個做法在講解 async 背後的運作機制,以及建議改用 async 來處理非同步相關的工作。

從古至今 C# 提供了多種非同步的寫法,例如: APMEAPTAP 每種寫法都有它的特色,不過為了使用上的方便,現在建議只使用 最新的 TAP 模式以及它的語法糖 asyncawait,也就是以任務為基礎的開發方式。

舊有的 APM 要求開發者要提供一個 BeginEnd 方法來代表非同步工作的開始與結束, EAP 則是透過 Event HandlerEventArg 這些事件相關的機制來達到非同步的流程,它們寫起來都與同步方法的寫法非常不同, 當使用 asyncawait 最大好處就是寫起來跟一般的同步方法幾乎一致,可以大大降低開發非同步程式的難度。

雖然寫起來與一般同步方法一樣,但加上 asyncawait 背後的運行順序會有極大的改變,例如下面這個例子中提供了一個 async 方法 MyDelay 並且在 Task.Delay 前添加了 await 模擬事件操作。

async Task Main()
{
	var task = MyDelay(3);
	Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " Before Await!");
	await task;
	Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " After Await!");
}

public async Task MyDelay(int second)
{
	Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " Before Delay!");
	await Task.Delay(TimeSpan.FromSeconds(second));
	Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " After Delay!");
}

以正常同步程式的邏輯來看會覺得結果是

  1. Before Delay!
  2. After Delay!
  3. Before Await!
  4. After Await!

但實際上的結果是

  1. Before Delay!
  2. Before Await!
  3. After Delay!
  4. After Await!

很明顯的執行的順序產生了改變,關鍵是在 await Task.Delay 執行時會在這段程式碼的位置留下標記並且可能會取得一個新的 Thread 執行這個非同步任務,這樣主要的執行緒就不用停在這個位置等待任務執行完畢,所以可以直接返回呼叫端執行之後的任務, 這也是 Before Delay!Before Await! 會同時輸出的原因。

接下來 await task 代表我想要取得執行結果,也就是真正等待三秒,完成後會透過之前留下來的標記返回之前中途離開的地點並執行之後的程式碼 ,一切都完成後才返回呼叫端並執行之後的程式碼,也是 After Delay! 會先輸出因為它是之前中斷的後續程式碼,最後才是 After Await!

也有可能 await 工作非常簡單所以很快就執行完了,這樣就沒必要取得新的 Thread 執行這個非同步任務,會有可能接續執行而不是跳回呼叫端, 例如你可以把等待時間調成零,這樣會變成類似同步的流程。

async Task Main()
{
	var task = MyDelay(0);
	Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " Before Await!");
	await task;
	Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " After Await!");
}

public async Task MyDelay(int second)
{
	Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " Before Delay!");
	await Task.Delay(TimeSpan.FromSeconds(second));
	Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " After Delay!");
}

這種留下標記並等未來完成後返回的特性是透過狀態機(State Machine) 來達成的,也就是說編譯器會把 async 方法轉換成狀態機, 然後把一些有用的參數和狀態保存到狀態機裡面,這樣就能做到任務完成時返回起始狀態的效果。

以下就是 MyDelay async 方法編譯出一個新的狀態機類別的程式碼,可以看到這個新類別實做了 IAsyncStateMachine 介面方法 MoveNextSetStateMachine, 參數 second 變成了成員,新增了 1__state 成員用來紀錄狀態等等變動。

比較重要的邏輯落在 GetAwaiterAwaitUnsafeOnCompleted 兩個方法,流程是運行方法後取得方法的 Awaiter,然後透過 AwaitUnsafeOnCompleted 方法告訴剛剛取得的 Awaiter 方法執行完成後需要呼叫 MoveNext 然後就回到呼叫端了,所以當呼叫端程式執行到 await task 的時候,就是在等待 Awaiter 呼叫 MoveNext,並完成剩下的邏輯。

在 .net core 已經不使用 SynchronizationContext 來處理非同步任務,而是改用 ExecutionContext

最後是錯誤捕捉,當非同步方法拋出例外錯誤的時候,編譯器會捕捉這些例外並將它們包裝在 Task 的 AggregateException 中,然後會把這個 Task 標記為 TaskStatus.Faulted,接下來如果使用者 await 這個失敗的 Task 就會拋出第一個例外錯誤,所以當 Task 有多個錯誤的同時產生, 這樣呼叫者需要自行解讀 AggregateException。

[AsyncStateMachine(typeof(<M>d__0))]
[DebuggerStepThrough]
public Task M()
{
    <M>d__0 stateMachine = new <M>d__0();
    stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
    stateMachine.<>4__this = this;
    stateMachine.<>1__state = -1;
    stateMachine.<>t__builder.Start(ref stateMachine);
    return stateMachine.<>t__builder.Task;
}
    
[AsyncStateMachine(typeof(<MyDelay>d__1))]
[DebuggerStepThrough]
public Task MyDelay(int second)
{
    <MyDelay>d__1 stateMachine = new <MyDelay>d__1();
    stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
    stateMachine.<>4__this = this;
    stateMachine.second = second;
    stateMachine.<>1__state = -1;
    stateMachine.<>t__builder.Start(ref stateMachine);
    return stateMachine.<>t__builder.Task;
}

[CompilerGenerated]
private sealed class <M>d__0 : IAsyncStateMachine
{
    public int <>1__state;

    public AsyncTaskMethodBuilder <>t__builder;

    [Nullable(0)]
    public C <>4__this;

    [Nullable(0)]
    private Task <task>5__1;

    private TaskAwaiter <>u__1;

    private void MoveNext()
    {
        int num = <>1__state;
        try
        {
            TaskAwaiter awaiter;
            if (num != 0)
            {
                <task>5__1 = <>4__this.MyDelay(3);
                Console.WriteLine(string.Concat(Thread.CurrentThread.ManagedThreadId.ToString(), " Before Await!"));
                awaiter = <task>5__1.GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    num = (<>1__state = 0);
                    <>u__1 = awaiter;
                    <M>d__0 stateMachine = this;
                    <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                    return;
                }
            }
            else
            {
                awaiter = <>u__1;
                <>u__1 = default(TaskAwaiter);
                num = (<>1__state = -1);
            }
            awaiter.GetResult();
            Console.WriteLine(string.Concat(Thread.CurrentThread.ManagedThreadId.ToString(), " After Await!"));
        }
        catch (Exception exception)
        {
            <>1__state = -2;
            <task>5__1 = null;
            <>t__builder.SetException(exception);
            return;
        }
        <>1__state = -2;
        <task>5__1 = null;
        <>t__builder.SetResult();
    }

    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);
    }
}
    
[CompilerGenerated]
private sealed class <MyDelay>d__1 : IAsyncStateMachine
{
    public int <>1__state;

    public AsyncTaskMethodBuilder <>t__builder;

    public int second;

    [Nullable(0)]
    public C <>4__this;

    private TaskAwaiter <>u__1;

    private void MoveNext()
    {
        int num = <>1__state;
        try
        {
            TaskAwaiter awaiter;
            if (num != 0)
            {
                Console.WriteLine(string.Concat(Thread.CurrentThread.ManagedThreadId.ToString(), " Before Delay!"));
                awaiter = Task.Delay(TimeSpan.FromSeconds(second)).GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    num = (<>1__state = 0);
                    <>u__1 = awaiter;
                    <MyDelay>d__1 stateMachine = this;
                    <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                    return;
                }
            }
            else
            {
                awaiter = <>u__1;
                <>u__1 = default(TaskAwaiter);
                num = (<>1__state = -1);
            }
            awaiter.GetResult();
            Console.WriteLine(string.Concat(Thread.CurrentThread.ManagedThreadId.ToString(), " After Delay!"));
        }
        catch (Exception exception)
        {
            <>1__state = -2;
            <>t__builder.SetException(exception);
            return;
        }
        <>1__state = -2;
        <>t__builder.SetResult();
    }

    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);
    }
}

Summary

雖然非同步程式有很多寫法沒有特別需求建議都是使用 asyncawait,另外編譯器會在你使用 asyncawait 的時候做許多額外的處理, 主要需要記得的關鍵就是狀態機以及中斷恢復的功能,還有同時產生多個錯誤處理需要自行解讀 AggregateException。