走進Task(2):Task 的回撥執行與 await

黑洞視界發表於2022-02-19

前言

本文為系列部落格

  1. 什麼是 Task
  2. Task 的回撥執行與 await(本文)
  3. async 到底幹了什麼(TODO)
  4. 總結與常見誤區(TODO)

上一篇我們講了對 Task 的基本定義:
Task 代表一個任務,其具體型別可能是多種多樣的,且有時候對我們來說完全是個黑盒。這個任務可以有結果,可以沒有結果,我們能知道這個任務什麼時候執行完成,並進行相應的後續處理。

Task 生命週期可以分為任務執行和回撥執行兩個主要的階段。上回講了 Task 的執行階段,這次我們就接著來看下回撥執行階段。

Task 將回撥函式維護在 m_continuationObject 欄位上,並通過 TrySetResult 等方法對外(這個對外僅限runtime裡Task相關的其他程式碼)暴露回撥的觸發方式。

由於 Task 的設計過於複雜,我的理解可能有錯誤,以後的版本可能會和現在有所出入。本文僅供參考學習,希望大家不要太過於糾結細節,瞭解設計思路比實現細節更重要。

class Task
{
    // 儲存一個或一組回撥
    private volatile object? m_continuationObject;

    internal bool TrySetResult()
    {
        // ...
        FinishContinuations();
        // ...
    }

    internal void FinishContinuations()
    {
        // 處理回撥的執行
    }
}

class Task<TResult> : Task
{
    internal bool TrySetResult(TResult result)
    {
        // ...
        this.m_result = result;
        // 複用父類的邏輯
        FinishContinuations();
        // ...
    }
}

本文要討論的其實就是對上述的補充:

  • Task 在把回撥函式儲存到 m_continuationObject 之前,對回撥函式進行了什麼樣的包裝處理?
  • Task 的 回撥函式是在什麼時候被觸發的,也就是 Task 的完成與回撥的執行是如何進行銜接的?
  • Task 所儲存的回撥函式會在哪裡執行?

Task.ContinueWith

往一個 Task 註冊回撥,有兩種方式:直接呼叫 Task 例項的 ContinueWith 方法,或者使用 await 關鍵詞。我們先看一下前者,await 放在後面單獨講。

ContinueWith 的產物:ContinuationTask

呼叫 ContinueWith 本質上是建立了一個新的 Task(後面簡稱為 ContinuationTask),而這個 ContinuationTask 的執行時間就是 原Task(後面簡稱為 AntecedentTask) 完成之後。

作為 Task ContinueWith 的返回值的 Task 的子類有以下四個,分別對應四種用法:

  1. ContinuationTaskFromTask
    向 Task 註冊一個回撥
Task task = Task.Run(() => Console.WriteLine("Hello"))
    .ContinueWith(t => Console.WriteLine("World"));

// System.Threading.Tasks.ContinuationTaskFromTask
Console.WriteLine(task.GetType());
  1. ContinuationResultTaskFromTask<TResult>
    向 Task 註冊一個回撥,並在回撥裡返回一個新值作為 新Task 的返回值
Task task = Task.Run(() => Console.WriteLine("Hello"))
    .ContinueWith(t => "World");

// System.Threading.Tasks.ContinuationResultTaskFromTask`1[System.String]
Console.WriteLine(task.GetType());

  1. ContinuationTaskFromResultTask<TAntecedentResult>
    向 Task<TResult> 註冊一個回撥, 並且 Task 獲取返回值
Task task = Task.Run(() => "Hello")
    .ContinueWith(t => Console.WriteLine($"{t.Result} World"));

// System.Threading.Tasks.ContinuationTaskFromResultTask`1[System.String]
Console.WriteLine(task.GetType());
  1. ContinuationResultTaskFromResultTask<TAntecedentResult, TResult>
    向 Task<TResult> 註冊一個回撥,並在回撥裡返回一個新值作為 新Task 的返回值
Task task = Task.Run(() => "Hello")
    .ContinueWith(t => $"{t.Result} World");

// System.Threading.Tasks.ContinuationResultTaskFromResultTask`2[System.String,System.String]
Console.WriteLine(task.GetType());

因為 Task.ContinueWith 的結果依舊是一個 Task,這個鏈式的回撥註冊可以無限地進行。

Task.Run(() => Console.WriteLine(1))
    .ContinueWith(t => Console.WriteLine(2))
    .ContinueWith(t => Console.WriteLine(3))
    .ContinueWith(t => Console.WriteLine(4));

額外的引數

class Task
{
    public Task ContinueWith(
        Action<Task> continuationAction,
        CancellationToken cancellationToken,
        TaskContinuationOptions continuationOptions,
        TaskScheduler scheduler)
        {
            // ...
        }
}

我們還可以通過 ContinueWith 的過載向其傳入回撥函式外的三個引數:

  • CancellationToken:協作式取消 Task 的執行,本文暫不展開。
  • TaskContinuationOptions:
    前一部分和 TaskCreationOptions 的值完全一致。
    如果設定的是這一部分的值,就會直接轉換為 ContinuationTask 的 TaskCreationOptions。TaskScheduler 識別過後進行相應的處理。
    如果設定的是後一部分的值,那麼 runtime 在決定把 Task 交給 TaskScheduler 去排程執行前,會根據設定的值做相應的預判邏輯。例如 OnlyOnFaulted 代表在 AntecedentTask 執行過程丟擲了異常,runtime 才會去執行 ContinuationTask。
public enum TaskCreationOptions
{
    None = 0,
    PreferFairness = 1,
    LongRunning = 2,
    AttachedToParent = 4,
    DenyChildAttach = 8,
    HideScheduler = 16, // 0x00000010
    RunContinuationsAsynchronously = 64, // 0x00000040
}

public enum TaskContinuationOptions
{
    None = 0,
    PreferFairness = 1,
    LongRunning = 2,
    AttachedToParent = 4,
    DenyChildAttach = 8,
    HideScheduler = 16, // 0x00000010
    LazyCancellation = 32, // 0x00000020
    RunContinuationsAsynchronously = 64, // 0x00000040
    // ---------- 分界線 ----------
    NotOnRanToCompletion = 65536, // 0x00010000
    NotOnFaulted = 131072, // 0x00020000
    NotOnCanceled = 262144, // 0x00040000
    OnlyOnRanToCompletion = NotOnCanceled | NotOnFaulted, // 0x00060000
    OnlyOnFaulted = NotOnCanceled | NotOnRanToCompletion, // 0x00050000
    OnlyOnCanceled = NotOnFaulted | NotOnRanToCompletion, // 0x00030000
    ExecuteSynchronously = 524288, // 0x00080000
}
  • TaskScheduler:可以之指定 TaskScheduler 去排程 Task。
    預設是 TaskScheduler.Current,而 TaskScheduler.Current 的預設值是 ThreadPoolTaskScheduler,可以修改成其他實現。

回撥的容器:TaskContinuation

我們注意到 m_continuationObject 欄位的型別是 object,而 object 型別在資料的儲存上有更多的靈活性。

class Task
{
    // 儲存一個或一組回撥
    private volatile object m_continuationObject;
}

我們看下下面的程式碼

var antecedentTask = Task.Run(() =>
{
    Thread.Sleep(1000);
    Console.WriteLine("Antecedent Task Completed");
});

PrintContinuationObjectType(antecedentTask);

antecedentTask.ContinueWith(_ => Console.WriteLine("Continuation Task1 Completed"));

PrintContinuationObjectType(antecedentTask);

antecedentTask.ContinueWith(_ => Console.WriteLine("Continuation Task2 Completed"));

PrintContinuationObjectType(antecedentTask);

Console.ReadLine();

void PrintContinuationObjectType(Task task)
{
    var continuationObject = typeof(Task)
        .GetField("m_continuationObject",
            BindingFlags.NonPublic | BindingFlags.Instance)
        .GetValue(task);

    var type = continuationObject?.GetType().FullName ?? "null";
    if (continuationObject is IEnumerable enumerable)
    {
        type += $", Element type: {enumerable.Cast<object>().First().GetType().FullName}";
    }

    Console.WriteLine(type);
}

執行結果如下

null
System.Threading.Tasks.ContinueWithTaskContinuation
System.Collections.Generic.List`1[[System.Object, System.Private.CoreLib, >Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], Element type: >System.Threading.Tasks.ContinueWithTaskContinuation
Antecedent Task Completed
Continuation Task1 Completed
Continuation Task2 Completed

隨著回撥函式註冊數量的增加,m_continuationObject 儲存的資料型別也在變化

  1. 沒有註冊時:null
  2. 一個回撥時:ContinueWithTaskContinuation 例項
  3. 超過一個回撥時:元素型別是 ContinueWithTaskContinuation 的 List<object>

實際上 m_continuationObject 還有別的型別:

class Task
{
    private void RunContinuations(object continuationObject) // separated out of FinishContinuations to enable it to be inlined
    {
        Debug.Assert(continuationObject != null);

        TplEventSource log = TplEventSource.Log;
        bool etwIsEnabled = log.IsEnabled();
        if (etwIsEnabled)
            log.TraceSynchronousWorkBegin(this.Id, CausalitySynchronousWork.CompletionNotification);

        bool canInlineContinuations =
            (m_stateFlags & (int)TaskCreationOptions.RunContinuationsAsynchronously) == 0 &&
            RuntimeHelpers.TryEnsureSufficientExecutionStack();

        switch (continuationObject)
        {
            // Handle the single IAsyncStateMachineBox case.  This could be handled as part of the ITaskCompletionAction
            // but we want to ensure that inlining is properly handled in the face of schedulers, so its behavior
            // needs to be customized ala raw Actions.  This is also the most important case, as it represents the
            // most common form of continuation, so we check it first.
            case IAsyncStateMachineBox stateMachineBox:
                AwaitTaskContinuation.RunOrScheduleAction(stateMachineBox, canInlineContinuations);
                LogFinishCompletionNotification();
                return;

            // Handle the single Action case.
            case Action action:
                AwaitTaskContinuation.RunOrScheduleAction(action, canInlineContinuations);
                LogFinishCompletionNotification();
                return;

            // Handle the single TaskContinuation case.
            case TaskContinuation tc:
                tc.Run(this, canInlineContinuations);
                LogFinishCompletionNotification();
                return;

            // Handle the single ITaskCompletionAction case.
            case ITaskCompletionAction completionAction:
                RunOrQueueCompletionAction(completionAction, canInlineContinuations);
                LogFinishCompletionNotification();
                return;
        }
}

ContinueWithTaskContinuation 的父類 TaskContinuation 是一個抽象類。除了 ContinueWithTaskContinuation,還有別的實現。

internal abstract class TaskContinuation
{
    internal abstract void Run(Task completedTask, bool canInlineContinuationTask);
}

ContinueWithTaskContinuation 維護著 Task 執行相關的兩個核心物件,一個是 Task 本身,另一是 TaskScheduler。真正執行回撥之前,需要先呼叫 TaskContinuation.Run。

internal sealed class ContinueWithTaskContinuation : TaskContinuation
{
    internal Task? m_task;
    internal readonly TaskContinuationOptions m_options;
    private readonly TaskScheduler m_taskScheduler;

    internal ContinueWithTaskContinuation(Task task, TaskContinuationOptions options, TaskScheduler scheduler)
    {
        m_task = task;
        m_options = options;
        m_taskScheduler = scheduler;
    }

    internal override void Run(Task completedTask, bool canInlineContinuationTask)
    {
        // ...
    }
}

Task.ContinueWith 回撥的生命週期

階段一 將回撥封裝進 ContinueWithTaskContinuation

我們向 Task 註冊的回撥回撥最終會以 ContinueWithTaskContinuation 的形式儲存在 Task 之中,相關的程式碼摘錄如下。其他 public 的 ContinueWith 可以看做是對這些 private 方法的封裝。

class Task
{
    private Task ContinueWith(Action<Task> continuationAction, TaskScheduler scheduler,
    CancellationToken cancellationToken, TaskContinuationOptions continuationOptions)
    {
        CreationOptionsFromContinuationOptions(continuationOptions, out TaskCreationOptions creationOptions, out InternalTaskOptions internalOptions);

        Task continuationTask = new ContinuationTaskFromTask(
            this, continuationAction, null,
            creationOptions, internalOptions
        );

        ContinueWithCore(continuationTask, scheduler, cancellationToken, continuationOptions);

        return continuationTask;
    }

    private Task<TResult> ContinueWith<TResult>(Func<Task, TResult> continuationFunction, TaskScheduler scheduler,
        CancellationToken cancellationToken, TaskContinuationOptions continuationOptions)
    {
        CreationOptionsFromContinuationOptions(continuationOptions, out TaskCreationOptions creationOptions, out InternalTaskOptions internalOptions);

        Task<TResult> continuationTask = new ContinuationResultTaskFromTask<TResult>(
            this, continuationFunction, null,
            creationOptions, internalOptions
        );

        ContinueWithCore(continuationTask, scheduler, cancellationToken, continuationOptions);

        return continuationTask;
    }

    internal void ContinueWithCore(Task continuationTask,
                                    TaskScheduler scheduler,
                                    CancellationToken cancellationToken,
                                    TaskContinuationOptions options)
    {
        // ...
        AddTaskContinuation(continuation);
        // ...
    }

    private bool AddTaskContinuation(object tc, bool addBeforeOthers)
    {
        // ...
        AddTaskContinuationComplex(tc, addBeforeOthers);
        // ...
    }

    private bool AddTaskContinuationComplex(object tc)
    {
        List<object?>? list = m_continuationObject as List<object?>;
        // ...
        list.Add(tc);
        // ...
    }
}

internal sealed class ContinuationTaskFromTask : Task
{
    private Task? m_antecedent;

    public ContinuationTaskFromTask(
        Task antecedent, Delegate action, object? state, TaskCreationOptions creationOptions, InternalTaskOptions internalOptions) :
        base(action, state, Task.InternalCurrentIfAttached(creationOptions), default, creationOptions, internalOptions, null)
    {
        m_antecedent = antecedent;
    }

    internal override void InnerInvoke()
    {
        if (m_action is Action<Task> action)
        {
            action(antecedent);
            return;
        }

        if (m_action is Action<Task, object?> actionWithState)
        {
            actionWithState(antecedent, m_stateObject);
            return;
        }
    }
}

子流程整理如下:

  1. 將委託包裝到具體的 ContinuationTask 例項裡(ContinuationTaskFromTask等 Task 的子類例項),
    定義 Task 子類的目的是為了將 AntecedentTask 的引用儲存起來,以便在執行 ContinuationTask 將 AntecedentTask 作為委託的引數傳入。
  2. 將 ContinuationTask 包裝到 ContinueWithTaskContinuation 例項中
  3. 將 ContinueWithTaskContinuation 新增到 TaskContinuation 列表裡(m_continuationObject)

階段二 回撥的觸發

這一部分其實就是上回 Task 可以封裝任何型別的別的任務 這一節提到的的流程:

  1. 排程器在執行完 AntecedentTask 之後,會去呼叫 AntecedentTask.TrySetResult()
  2. 在 TrySetResult 方法裡,最終會去呼叫 TaskContinuation.Run()
  3. ContinueWithTaskContinuation 裡會把 ContinuationTask 放入 ContinueWithTaskContinuation 裡維護的 TaskScheduler 裡排程執行。

回撥執行真正的決定者:ContinueWithTaskContinuation

在 ContinueWithTaskContinuation 中維護著待執行的 ContinuationTask 以及決定 ContinuationTask 最終執行方式的 TaskContinuationOptions 和 TaskScheduler。

internal sealed class ContinueWithTaskContinuation : TaskContinuation
{
    internal Task? m_task;
    internal readonly TaskContinuationOptions m_options;
    private readonly TaskScheduler m_taskScheduler;

    internal ContinueWithTaskContinuation(Task task, TaskContinuationOptions options, TaskScheduler scheduler)
    {
        m_task = task;
        m_options = options;
        m_taskScheduler = scheduler;
    }

    internal override void Run(Task completedTask, bool canInlineContinuationTask)
    {
        Task? continuationTask = m_task;
        m_task = null;

        // 檢查任務的完成狀態,如果不符合 TaskContinuationOptions 的設定,回撥就不會被執行
        TaskContinuationOptions options = m_options;
        bool isRightKind =
            completedTask.IsCompletedSuccessfully ?
                (options & TaskContinuationOptions.NotOnRanToCompletion) == 0 :
                (completedTask.IsCanceled ?
                    (options & TaskContinuationOptions.NotOnCanceled) == 0 :
                    (options & TaskContinuationOptions.NotOnFaulted) == 0);

        // 任務完成狀態符合要求,回撥執行。
        if (isRightKind)
        {
            continuationTask.m_taskScheduler = m_taskScheduler;

            // 直接執行回撥或將其排隊等待執行,具體取決於是否需要同步或非同步執行。
            // 預設執行路徑,上層傳的是 true。
            if (canInlineContinuationTask && // 呼叫Run方法的內部方法傳了允許內聯
                (options & TaskContinuationOptions.ExecuteSynchronously) != 0) // 註冊回撥的實際使用者設定了同步執行
            {
                InlineIfPossibleOrElseQueue(continuationTask, needsProtection: true);
            }
            else
            {
                try { continuationTask.ScheduleAndStart(needsProtection: true); }
                catch (TaskSchedulerException)
                {
                    // 如果 Task 執行失敗了,ScheduleAndStart 方法會將 Task 標記為失敗,
                    // 這裡是runtime設計的時候保證不會有意外的錯誤發生,僅做catch,不做處理
                }
            }
        }
        else
        {
            Task.ContingentProperties? cp = continuationTask.m_contingentProperties;
            if (cp is null || cp.m_cancellationToken == default)
            {
                continuationTask.InternalCancelContinueWithInitialState();
            }
            else
            {
                continuationTask.InternalCancel();
            }
        }
    }
}

所謂的 Inline 是指在觸發回撥的執行緒中直接執行回撥。
像 Task.Run 建立的 Task(由 ThreadPoolTaskScheduler 排程,也就是由執行緒池排程) 的回撥如果是 Inline 執行的話,那執行回撥的執行緒和執行傳給 Task.Run 的委託的執行緒,就會是同一個執行緒池執行緒。因為執行緒池在執行完委託之後,就會觸發回撥執行。

我們註冊的 TaskScheduler 可以選擇是否只是 Inline。

public abstract class TaskScheduler
{
    // 如果不是 Inline 執行,就是走這個方法執行回撥
    // 如果沒有傳
    protected internal abstract void QueueTask(Task task);

    // 如果返回 false,就算引數要求 Inline ,也會走 QueueTask 執行回撥
    protected abstract bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued);

    // 獲取所有排程到該 TaskScheduler 的 Task
    protected abstract IEnumerable<Task>? GetScheduledTasks();
}

執行回撥的執行緒

根據上文的吻戲 Task.ContinueWith 的回撥最終在哪執行取決於 TaskContinuationOptions 和 TaskScheduler。

下面是幾個典型的例子:

  1. Inline
Task.Run(() =>
    {
        Thread.Sleep(1000);
        Console.WriteLine($"Task Run, ThreadId: {Environment.CurrentManagedThreadId}");
    })
    .ContinueWith(t => Console.WriteLine($"Task OnCompleted, ThreadId: {Environment.CurrentManagedThreadId}"),
        TaskContinuationOptions.ExecuteSynchronously);

Console.ReadKey();
前後執行緒永遠不會發生變化
Task Run, ThreadId: 6
Task OnCompleted, ThreadId: 6
  1. 排程到 ThreadPool 本地佇列
    下面的例子裡,也就是排程到執行前一個執行前一個委託的執行緒池執行緒的本地佇列裡
Task.Run(() =>
    {
        Thread.Sleep(1000);
        Console.WriteLine($"Task Run, ThreadId: {Environment.CurrentManagedThreadId}");
    })
    .ContinueWith(t => Console.WriteLine($"Task OnCompleted, ThreadId: {Environment.CurrentManagedThreadId}")); // 預設是 TaskContinuationOptions.None

Console.ReadKey();

有可能前後是一個執行緒,也有可能不是,可以多執行幾次看看。
更多說明請看 ThreadPool 的部落格中偷竊機制

  1. 排程到 ThreadPool 全域性佇列
Task.Run(() =>
    {
        Thread.Sleep(1000);
        Console.WriteLine($"Task Run, ThreadId: {Environment.CurrentManagedThreadId}");
    })
    .ContinueWith(t => Console.WriteLine($"Task OnCompleted, ThreadId: {Environment.CurrentManagedThreadId}"),
        TaskContinuationOptions.PreferFairness);

將回撥排程到全域性佇列,等待執行緒池執行緒領取並執行。

Task 與 await

與 ContinueWith 相比,await 給我們提供了更加簡單的 Task 的使用方式。

Task.Run(() => "Hello")
    .ContinueWith(t => Console.WriteLine($"{t.Result} World"));
// 等效於
var result = await Task.Run(() => "Hello");
Console.WriteLine($"{result} World");

Awaiter

我們可以通過 Task.GetAwaiter 從 Task 例項上獲取到 Task 對應的 TaskAwaiter 物件。並且可以通過 TaskAwaiter.OnCompleted 方法註冊回撥,其執行結果與 Task.ContinueWith 一致。

TaskAwaiter awaiter1 = Task.Run(()=> Console.WriteLine("Hello")).GetAwaiter();
awaiter1.OnCompleted(()=> Console.WriteLine("World"));

TaskAwaiter<string> awaiter2 = Task.Run(()=> "Hello").GetAwaiter();
awaiter2.OnCompleted(()=> Console.WriteLine($"{awaiter2.GetResult()} World"));

Console.ReadKey();
Hello World
Hello
World

注意:直接呼叫 TaskAwaiter.GetResult 會阻塞呼叫執行緒直至 Task 執行完成。

TaskAwaiter 本質上可以理解成在 await 語法糖編譯成的程式碼中,為了解耦 Task 和狀態機,而建立的一個隔離層,內部對 Task 進行了包裝。

public class Task<TResult>
{
    public TaskAwaiter<TResult> GetAwaiter() => new TaskAwaiter<TResult>(this);

    internal void SetContinuationForAwait(
        Action continuationAction, bool continueOnCapturedContext, bool flowExecutionContext)
    {
        TaskContinuation? tc = null;

        if (continueOnCapturedContext)
        {
            SynchronizationContext? syncCtx = SynchronizationContext.Current;
            if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext))
            {
                tc = new SynchronizationContextAwaitTaskContinuation(syncCtx, continuationAction, flowExecutionContext);
            }
            else
            {
                TaskScheduler? scheduler = TaskScheduler.InternalCurrent;
                if (scheduler != null && scheduler != TaskScheduler.Default)
                {
                    tc = new TaskSchedulerAwaitTaskContinuation(scheduler, continuationAction, flowExecutionContext);
                }
            }
        }

        if (tc == null && flowExecutionContext)
        {
            tc = new AwaitTaskContinuation(continuationAction, flowExecutionContext: true);
        }

        if (tc != null)
        {
            if (!AddTaskContinuation(tc, addBeforeOthers: false))
                tc.Run(this, canInlineContinuationTask: false);
        }
        else
        {
            if (!AddTaskContinuation(continuationAction, addBeforeOthers: false))
                AwaitTaskContinuation.UnsafeScheduleAction(continuationAction, this);
        }
    }
}

public readonly struct TaskAwaiter<TResult> :
    ICriticalNotifyCompletion,
    INotifyCompletion,
    ITaskAwaiter
{
    private readonly Task<TResult> m_task;

    internal TaskAwaiter(Task task)
    {
        m_task = task;
    }

    public bool IsCompleted => m_task.IsCompleted;

    public void OnCompleted(Action continuation)
    {
        TaskAwaiter.OnCompletedInternal(m_task, continuation, continueOnCapturedContext: true,
            flowExecutionContext: true);
    }

    public void UnsafeOnCompleted(Action continuation)
    {
        TaskAwaiter.OnCompletedInternal(m_task, continuation, continueOnCapturedContext: true,
            flowExecutionContext: false);
    }

    [StackTraceHidden]
    public TResult GetResult()
    {
        TaskAwaiter.ValidateEnd((Task)this.m_task);
        return this.m_task.ResultOnSuccess;
    }

    internal static void OnCompletedInternal(
        Task task,
        Action continuation,
        bool continueOnCapturedContext,
        bool flowExecutionContext)
    {
        task.SetContinuationForAwait(continuation, continueOnCapturedContext, flowExecutionContext);
    }
}

可以看到 TaskAwaiter.OnCompleted 就是往 Task 註冊回撥,而 await 關鍵詞的本質就是把 await 後面的程式碼變成了回撥並註冊到了 Task 上。

Task.Run(() => "Hello")
    .ContinueWith(t => Console.WriteLine($"{t.Result} World"));
// 等效於
var result = await Task.Run(() => "Hello");
Console.WriteLine($"{result} World");
// 等效於
Task.Run(()=> "Hello").GetAwaiter().OnCompleted(()=> Console.WriteLine("World"));

至於 TaskAwaiter.UnsafeOnCompleted 我們稍後解釋。

await Anything

C# 編譯器並沒有限制 await 關鍵詞只能用在 Task 上。例如 Task.Yield() 的返回值 YieldAwaitable,既不是 Task 也不是 Task 的子類。

public readonly struct YieldAwaitable
{
    public YieldAwaitable.YieldAwaiter GetAwaiter() => new YieldAwaitable.YieldAwaiter();

    public readonly struct YieldAwaiter :
        ICriticalNotifyCompletion,
        INotifyCompletion
    {
        public bool IsCompleted => false;

        public void OnCompleted(Action continuation) => YieldAwaitable.YieldAwaiter.QueueContinuation(continuation, true);

        public void UnsafeOnCompleted(Action continuation) => YieldAwaitable.YieldAwaiter.QueueContinuation(continuation, false);
        
        public void GetResult()
        {
        }
    }
}

Task 和 YieldAwaitable 都提供了一個 GetAwaiter 方法。
返回的 XXXAwaiter 需滿足以下兩個條件:

  1. ICriticalNotifyCompletion,INotifyCompletion 這兩個介面。而 ICriticalNotifyCompletion 是 INotifyCompletion 的子介面。
public interface INotifyCompletion
{
    void OnCompleted(Action continuation);
}

public interface ICriticalNotifyCompletion : INotifyCompletion
{
    void UnsafeOnCompleted(Action continuation);
}
  1. 提供 IsCompleted 屬性 和 void GetResult() / TResult GetResult() 方法。GetResult 方法是否有返回值取決於 await XXXAwaitable 是否想提供返回值。

實際上,我們自己想要實現一個 Awaitable 的話,Awaiter 只需要實現 INotifyCompletion 介面或者 ICriticalNotifyCompletion 就可以了。

首先,我們需要準備好一個 Awaitable。

class FooAwaitable<TResult>
{
    // 回撥,簡化起見,未將其包裹到 TaskContinuation 這樣的容器裡
    private Action _continuation;

    private TResult _result;

    private volatile bool _completed;

    public bool IsCompleted => _completed;

    // Awaitable 中的關鍵部分,提供 GetAwaiter 方法
    public FooAwaiter<TResult> GetAwaiter() => new FooAwaiter<TResult>(this);

    public void Run(Func<TResult> func)
    {
        new Thread(() =>
        {
            var result = func();
            TrySetResult(result);
        })
        {
            IsBackground = true
        }.Start();
    }

    private bool AddFooContinuation(Action action)
    {
        if (_completed)
        {
            return false;
        }
        _continuation += action;
        return true;
    }

    private void TrySetResult(TResult result)
    {
        _result = result;
        _completed = true;
        _continuation?.Invoke();
    }

    // TODO: 實現一個 FooAwaiter 作為 FooAwaitable 內部類
    // public struct FooAwaiter<TResult> : INotifyCompletion Or ICriticalNotifyCompletion
    // {
    // }
}

實現 INotifyCompletion 介面的 Awaiter 示例

var fooAwaitable = new FooAwaitable<string>();

fooAwaitable.Run(() =>
{
    // 可以把Sleep去掉看看
    Thread.Sleep(100);
    Console.WriteLine("Hello");
    return "World";
});

var x = await fooAwaitable;
Console.WriteLine(x);

Console.ReadKey();

class FooAwaitable<TResult>
{
    // ...
    // 上面所展示的 FooAwaitable 裡的程式碼,此處省略
    // ...

    // 1. 實現 INotifyCompletion
    public struct FooAwaiter<TResult> : INotifyCompletion
    {
        private readonly FooAwaitable<TResult> _fooAwaitable;
        
        // 2. 實現 IsCompleted 屬性
        public bool IsCompleted => _fooAwaitable.IsCompleted;

        public FooAwaiter(FooAwaitable<TResult> fooAwaitable)
        {
            _fooAwaitable = fooAwaitable;
        }

        public void OnCompleted(Action continuation)
        {
            Console.WriteLine("FooAwaiter.OnCompleted");
            if (_fooAwaitable.AddFooContinuation(continuation))
            {
                Console.WriteLine("FooAwaiter.OnCompleted: added continuation");
            }
            else
            {
                // 試著把上面的 Thread.Sleep(100) 刪掉看看,就有可能會執行到這裡
                // 也就是回撥的註冊時間有可能晚於任務完成的時間
                Console.WriteLine("FooAwaiter.OnCompleted: already completed, invoking continuation");
                continuation();
            }
        }
        
        // 3. 實現 GetResult 方法
        public TResult GetResult()
        {
            Console.WriteLine("FooAwaiter.GetResult");
            return _fooAwaitable._result;
        }
    }
}

執行結果如下:

FooAwaiter.OnCompleted
FooAwaiter.OnCompleted: added continuation
Hello
FooAwaiter.GetResult
World

實現 ICriticalNotifyCompletion 介面的 Awaiter 示例

var fooAwaitable = new FooAwaitable<string>();

fooAwaitable.Run(() =>
{
    Thread.Sleep(100);
    Console.WriteLine("Hello");
    return "World";
});

var x = await fooAwaitable;
Console.WriteLine(x);

Console.ReadKey();

class FooAwaitable<TResult>
{
    // ...
    // 上面所展示的 FooAwaitable 裡的程式碼,此處省略
    // ...

    // 1 實現 ICriticalNotifyCompletion
    public struct FooAwaiter<TResult> : ICriticalNotifyCompletion
    {
        private readonly FooAwaitable<TResult> _fooAwaitable;
        
        // 2 實現 IsCompleted 屬性
        public bool IsCompleted => _fooAwaitable.IsCompleted;

        public FooAwaiter(FooAwaitable<TResult> fooAwaitable)
        {
            _fooAwaitable = fooAwaitable;
        }

        public void OnCompleted(Action continuation)
        {
            Console.WriteLine("FooAwaiter.OnCompleted");
            if (_fooAwaitable.AddFooContinuation(continuation))
            {
                Console.WriteLine("FooAwaiter.OnCompleted: added continuation");
            }
            else
            {
                Console.WriteLine("FooAwaiter.OnCompleted: already completed, invoking continuation");
                continuation();
            }
        }

        public void UnsafeOnCompleted(Action continuation)
        {
            Console.WriteLine("FooAwaiter.UnsafeOnCompleted");
            if (_fooAwaitable.AddFooContinuation(continuation))
            {
                Console.WriteLine("FooAwaiter.UnsafeOnCompleted: added continuation");
            }
            else
            {
                Console.WriteLine("FooAwaiter.UnsafeOnCompleted: already completed, invoking continuation");
                continuation();
            }
        }

        // 3. 實現 GetResult 方法
        public TResult GetResult()
        {
            Console.WriteLine("FooAwaiter.GetResult");
            return _fooAwaitable._result;
        }
    }
}

執行結果如下:

FooAwaiter.UnsafeOnCompleted
FooAwaiter.UnsafeOnCompleted: added continuation
Hello
FooAwaiter.GetResult
World

一旦實現了 ICriticalNotifyCompletion(INotifyCompletion 的子介面),註冊回撥走的是 UnsafeOnCompleted 方法。如果同時實現兩個方法,也還是以ICriticalNotifyCompletion 的規則優先。

INotifyCompletion VS ICriticalNotifyCompletion

既然實現 Awaitable 只要實現兩個介面之一,那為什麼要區分出這兩個介面呢。
我們來看看 TaskAwaiter 裡的實現是什麼樣。

public readonly struct TaskAwaiter<TResult> : ICriticalNotifyCompletion, INotifyCompletion
{
    private readonly Task<TResult> m_task;

    internal TaskAwaiter(Task<TResult> task)
    {
        m_task = task;
    }

    // ...

    public void OnCompleted(Action continuation)
    {
        TaskAwaiter.OnCompletedInternal(m_task, continuation, continueOnCapturedContext: true, flowExecutionContext: true);
    }

    public void UnsafeOnCompleted(Action continuation)
    {
        TaskAwaiter.OnCompletedInternal(m_task, continuation, continueOnCapturedContext: true, flowExecutionContext: false);
    }
    
    internal static void OnCompletedInternal(
        Task task,
        Action continuation,
        bool continueOnCapturedContext,
        bool flowExecutionContext)
    {
        m_task.SetContinuationForAwait(continuation, continueOnCapturedContext, flowExecutionContext);
    }
    // ...
}

OnCompleted 和 UnsafeOnCompleted 的唯一區別是在呼叫 TaskAwaiter.OnCompletedInternal 時,flowExecutionContext 這個引數有所不同。

ExecutionContext 的本質是一個執行緒私有變數,維護著我們常用 AsyncLocal 的資料,例如 Thread.CurrentThread.CurrentCulture 其實就是一個 AsyncLocal 變數。

runtime 中會在發生執行緒切換的地方,將 ExecutionContext 從前一個執行緒拷貝到後一個執行緒。那麼第二個執行緒裡也就可以拿到在第一個執行緒裡設定好的 AsyncLocal 變數。

就算執行緒沒有發生切換,runtime 裡有的地方也會通過清空 ExecutionContext 來阻止其往後傳播。

更多 ExcutionContext 和 AsyncLocal 的解析,請參考我之前的一篇部落格:
https://www.cnblogs.com/eventhorizon/p/12240767.html

也就是說 OnCompleted 會保證 ExecutionContext 往後傳播。而 UnsafeOnCompleted 則不會。我們來看下面這個示例。

class Program
{
    private static readonly AsyncLocal<string> AsyncLocal = new AsyncLocal<string>();

    static void Main(string[] args)
    {
        AsyncLocal.Value = "Hello World";

        Task.Run(() =>
            {
                Thread.Sleep(1000);
                Console.WriteLine(
                    $"Task1 Run, ThreadId: {Environment.CurrentManagedThreadId}, AsyncLocal: {AsyncLocal.Value}");
            })
            .GetAwaiter()
            .OnCompleted(() =>
                Console.WriteLine(
                    $"Task1 OnCompleted, ThreadId: {Environment.CurrentManagedThreadId}, AsyncLocal: {AsyncLocal.Value}"));

        Task.Run(() =>
            {
                Thread.Sleep(1000);
                Console.WriteLine(
                    $"Task2 Run, ThreadId: {Environment.CurrentManagedThreadId}, AsyncLocal: {AsyncLocal.Value}");
            })
            .GetAwaiter()
            .UnsafeOnCompleted(() =>
                Console.WriteLine(
                    $"Task2 UnsafeOnCompleted, ThreadId: {Environment.CurrentManagedThreadId}, AsyncLocal: {AsyncLocal.Value}"));

        Console.ReadKey();
    }
}
Task1 Run, ThreadId: 6, AsyncLocal: Hello World
Task2 Run, ThreadId: 7, AsyncLocal: Hello World
Task1 OnCompleted, ThreadId: 6, AsyncLocal: Hello World
Task2 UnsafeOnCompleted, ThreadId: 7, AsyncLocal: 

如果使用了 UnsafeOnCompleted 註冊回撥,也就是 flowExecutionContext: false,則 ExecutionContext 不會往後繼續傳播。

同一個 Task 回撥執行前後執行緒沒變是因為 TaskSchedulerAwaitTaskContinuation 裡優先 Inline 執行回撥,暫不展開。

AsyncTaskMethodBuilder 是狀態機的一個重要組成部分,負責 狀態機與 awaiter 的銜接工作,更詳細的功能我們下篇部落格再敘述,這邊只簡單提一下。
AsyncTaskMethodBuilder 主要負責以下功能:

  1. 將 async 方法內部的返回值封裝到 async 方法的最終所返回的 Task 中,並作為這個 Task 的返回值。
  2. 將 async 方法內部發生的異常 封裝到 async 方法的最終所返回的 Task 中。
  3. 將狀態機待執行的動作作為回撥 向 awaiter 註冊(awaiter 內部再向 Task 註冊)。

我們可以給 async 方法內部的狀態機自己繫結 AsyncMethodBuilder。在自定義的 AsyncTaskMethodBuilder 裡可以決定要不要往後傳 ExecutionContext.UnsafeOnCompleted 這個方法的存在意義就是為了在我們不像往後傳 ExecutionContext 的時候使用。

async 方法 內的 AsyncMethodBuilder 和 async 方法的返回值有關,AsyncMethodBuilder 繫結在作為返回值的 Awaitable 上,下篇再講。

就目前 .NET 6 的程式碼來說, async Task FooAsync(){} 這樣的以 Task 作為返回值的 async 方法中的狀態機來說,Task 方法所繫結的 AsyncMethodBuilder 內並沒有呼叫 TaskAwaiter.UnsafeOnCompleted 方法,而是通過其他方式註冊的回撥,大致的流程和使用 TaskAwaiter.UnsafeOnCompleted 進行註冊時類似的。
如果像上文那樣自己實現 Awaitable,會呼叫 TaskAwaiter.OnCompleted 或者 TaskAwaiter.OnCompleted 方法。這個和 AsyncMethodBuilder 內部的實現有關。(手動狗頭,設計的太複雜了)

有限元狀態機

下面是摘自百度百科的關於狀態機的說明:

狀態機可歸納為4個要素,即現態、條件、動作、次態。這樣的歸納,主要是出於對狀態機的內在因果關係的考慮。“現態”和“條件”是因,“動作”和“次態”是果。詳解如下:

  1. 現態:是指當前所處的狀態。
  2. 條件:又稱為“事件”,當一個條件被滿足,將會觸發一個動作,或者執行一次狀態的遷移。
  3. 動作:條件滿足後執行的動作。動作執行完畢後,可以遷移到新的狀態,也可以仍舊保持原狀態。動作不是必需的,當條件滿足後,也可以不執行任何動作,直接遷移到新狀態。
  4. 次態:條件滿足後要遷往的新狀態。“次態”是相對於“現態”而言的,“次態”一旦被啟用,就轉變成新的“現態”了。

而有限元狀態機的有限是指狀態的有限。

觀察下面這麼一個常見的 await 使用場景,可以將 FooAsync 方法內部的邏輯分為三種狀態(即 三個階段):

  1. 初始化狀態
  2. 等待 BarAsync 執行完成的狀態
  3. 執行結束狀態
class Program
{
    static async Task Main(string[] args)
    {
        var a = 1;
        Console.WriteLine(await FooAsync(a));
    }

    static async Task<int> FooAsync(int a)
    {
        int b = 2;
        int c = await BarAsync();
        return a + b + c;
    }

    static async Task<int> BarAsync()
    {
        await Task.Delay(100);
        return 3;
    }
}

由 FooAsync 編譯成的 IL 程式碼經整理後的等效 C# 程式碼如下:

using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        var a = 1;
        Console.WriteLine(await FooAsync(a));
    }

    static Task<int> FooAsync(int a)
    {
        var stateMachine = new FooStateMachine
        {
            _asyncTaskMethodBuilder = AsyncTaskMethodBuilder<int>.Create(),
    
            _state = -1, // 初始化狀態
            _a = a // 將實參拷貝到狀態機欄位
        };
        // 開始執行狀態機
        stateMachine._asyncTaskMethodBuilder.Start(ref stateMachine);
        return stateMachine._asyncTaskMethodBuilder.Task;
    }

    static async Task<int> BarAsync()
    {
        await Task.Delay(100);
        return 3;
    }

    public class FooStateMachine : IAsyncStateMachine
    {
        // 方法的引數和區域性變數被編譯會欄位
        public int _a;
        public AsyncTaskMethodBuilder<int> _asyncTaskMethodBuilder;
        private int _b;

        private int _c;

        // -1: 初始化狀態
        // 0: 等到 Task 執行完成
        // -2: 狀態機執行完成
        public int _state;

        private TaskAwaiter<int> _taskAwaiter;

        public void MoveNext()
        {
            var result = 0;
            TaskAwaiter<int> taskAwaiter;
            try
            {
                // 狀態不是0,代表 Task 未完成
                if (_state != 0)
                {
                    // 初始化區域性變數
                    _b = 2;

                    taskAwaiter = Program.BarAsync().GetAwaiter();
                    if (!taskAwaiter.IsCompleted)
                    {
                        // state: -1 => 0,非同步等待 Task 完成
                        _state = 0;
                        _taskAwaiter = taskAwaiter;
                        var stateMachine = this;
                        // 內部會呼叫 將 stateMachine.MoveNext 註冊為 Task 的回撥
                        _asyncTaskMethodBuilder.AwaitUnsafeOnCompleted(ref taskAwaiter, ref stateMachine);
                        return;
                    }
                }
                else
                {
                    taskAwaiter = _taskAwaiter;
                    // TaskAwaiter 是個結構體,這邊相當於是個清空 _taskAwaiter 欄位的操作
                    _taskAwaiter = new TaskAwaiter<int>();
                    // state: 0 => -1,狀態機恢復到初始化狀態
                    _state = -1;
                }

                _c = taskAwaiter.GetResult();
                result = _a + _b + _c;
            }
            catch (Exception e)
            {
                // state: any => -2,狀態機執行完成
                _state = -2;
                _asyncTaskMethodBuilder.SetException(e);
                return;
            }

            // state: -1 => -2,狀態機執行完成
            _state = -2;
            // 將 result 設定為 FooAsync 方法的返回值
            _asyncTaskMethodBuilder.SetResult(result);
        }

        public void SetStateMachine(IAsyncStateMachine stateMachine)
        {
        }
    }
}

編譯器在 Program 中建立了一個內部類,也就是 FooStateMachine 這個狀態機,而 FooAsync 方法則變成了對這個狀態機的使用。
AsyncTaskMethodBuilder 的作用解釋放到下一篇文章再解釋,這邊簡單理解成 AsyncTaskMethodBuilder.SetResult 就是 FooAsync return 返回值,AsyncTaskMethodBuilder.SetException 就是 FooAsync 內部往外扔異常。

完整的流程如下圖所示:

一個方法中就算有個 await,這個方法也只會有一個對應的狀態機。就.NET 6 SDK 的編譯結果來看,state 會出現 -1 => 0(等待第一個Task非同步執行完成) => -1 => 0(等待第二個Task非同步執行完成)這樣的流程。

AsyncStateMachineBox

前文講過 awaiter 往 Task 註冊回撥的邏輯裡,可能不會直接傳遞 ExcutionContext。
而這個 AsyncStateMachineBox 是對 AsyncStateMachine 和 ExcutionContext 的包裝,這邊通過這樣的方式往後傳遞 ExcutionContext。

await Task 的回撥在哪執行

回憶一下上文 Task.ContinueWith 講回撥最終封裝到了 ContinueWithTaskContinuation。

返回值是 Task 的情況下狀態機所繫結的 AsyncTaskMethodBuilder 的所會呼叫 Task.UnSafeSetContinuationForAwait 例項方法。裡面會根據不同的條件建立不同的 TaskContinuation。

UnSafeSetContinuationForAwait 中的邏輯和後續回撥執行流程大致如下:

同步上下文(SynchronizationContext)導致的死鎖問題與 Task.ConfigureAwait(continueOnCapturedContext:false)

如果存在 SynchronizationContext,回撥會優先在 SynchronizationContext 上執行。而 SynchronizationContext 也是一種任務排程器,其存在時間應該是早於 Task 的。

在 .NET Framework 時代的 WPF、Windows Form、Asp.NET Web Form 這些框架裡,都有 SynchronizationContext 的存在。

下面是一個 SynchronizationContext 的實現示例:

class SingleThreadedSynchronizationContext : SynchronizationContext
{
    private readonly BlockingCollection<(SendOrPostCallback Callback, object State)> _queue = new BlockingCollection<(SendOrPostCallback Callback, object State)>();

    public override void Send(SendOrPostCallback d, object state) // Sync operations
    {
        throw new NotSupportedException($"{nameof(SingleThreadedSynchronizationContext)} does not support synchronous operations.");
    }

    public override void Post(SendOrPostCallback d, object? state) // Async operations
    {
        _queue.Add((d, state));
    }

    public static void Run(Action action)
    {
        var previous = Current;
        var context = new SingleThreadedSynchronizationContext();
        SetSynchronizationContext(context);
        try
        {
            Console.WriteLine("Executing first action, CurrentThreadId: {0}", Environment.CurrentManagedThreadId);
            action();

            while (context._queue.TryTake(out var item))
            {
                Console.WriteLine("Executing callback, CurrentThreadId: {0}", Environment.CurrentManagedThreadId);
                item.Callback(item.State);
            }
        }
        finally
        {
            context._queue.CompleteAdding();
            SetSynchronizationContext(previous);
        }
    }
}

WPF 這些框架裡,UI 只允許 UI 執行緒去更新。這些 SynchronizationContext 有個特點,就是一次只允許一個任務執行。

class Program
{
    private static void Main(string[] args)
    {
        new Thread(() =>
        {
            Console.WriteLine("Thread started, CurrentThreadId: {0}", Environment.CurrentManagedThreadId);
            SingleThreadedSynchronizationContext.Run(Test);
        })
        {
            IsBackground = true
        }.Start();
        Console.ReadKey();
    }

    private static void Test()
    {
        Console.WriteLine("Test: START, CurrentThreadId: {0}", Environment.CurrentManagedThreadId);
        Console.WriteLine($"Test.SynchronizationContext1: {SynchronizationContext.Current}");
        // 時間點一:這裡把唯一的執行執行緒給阻塞住了,會導致死鎖
        DoSthAsync().GetAwaiter().GetResult();
        Console.WriteLine($"Test.SynchronizationContext2: {SynchronizationContext.Current}");
        Console.WriteLine("Test: END, CurrentThreadId: {0}", Environment.CurrentManagedThreadId);
    }

    private static async Task DoSthAsync()
    {
        Console.WriteLine("DoSthAsync: START, CurrentThreadId: {0}", Environment.CurrentManagedThreadId);
        Console.WriteLine($"DoSthAsync.SynchronizationContext1: {SynchronizationContext.Current}");
        // await 後面的程式碼作為 Task.Delay 的回撥,
        // 等待 Task.Delay 結束後會由 MaxConcurrencySynchronizationContext 進行排程執行
        await Task.Delay(100);
        // 時間點二:MaxConcurrencySynchronizationContext 唯一的執行緒已經被阻塞住了,死鎖開始
        Console.WriteLine($"DoSthAsync.SynchronizationContext2: {SynchronizationContext.Current}");
        Console.WriteLine("DoSthAsync: END, CurrentThreadId: {0}", Environment.CurrentManagedThreadId);
    }
}

執行結果如下:

Thread started, CurrentThreadId: 10
Executing first action, CurrentThreadId: 10
Test: START, CurrentThreadId: 10
Test.SynchronizationContext1: SingleThreadedSynchronizationContext
DoSthAsync: START, CurrentThreadId: 10
DoSthAsync.SynchronizationContext1: SingleThreadedSynchronizationContext

await Task.Delay(100) 的回撥將無法被執行。

那麼如何在這些 UI 框架裡避免死鎖呢?我們只需要將 await Task.Delay(100) 改為 await Task.Delay(100).ConfigureAwait(continueOnCapturedContext:false)

class Program
{
    private static void Main(string[] args)
    {
        new Thread(() =>
        {
            Console.WriteLine("Thread started, CurrentThreadId: {0}", Environment.CurrentManagedThreadId);
            SingleThreadedSynchronizationContext.Run(Test);
        })
        {
            IsBackground = true
        }.Start();
        Console.ReadKey();
    }

    private static void Test()
    {
        Console.WriteLine("Test: START, CurrentThreadId: {0}", Environment.CurrentManagedThreadId);
        Console.WriteLine($"Test.SynchronizationContext1: {SynchronizationContext.Current}");
        // 時間點一:這裡把唯一的執行執行緒給阻塞住了,但不會導致死鎖
        DoSthAsync().GetAwaiter().GetResult();
        Console.WriteLine($"Test.SynchronizationContext2: {SynchronizationContext.Current}");
        Console.WriteLine("Test: END, CurrentThreadId: {0}", Environment.CurrentManagedThreadId);
    }

    private static async Task DoSthAsync()
    {
        Console.WriteLine("DoSthAsync: START, CurrentThreadId: {0}", Environment.CurrentManagedThreadId);
        Console.WriteLine($"DoSthAsync.SynchronizationContext1: {SynchronizationContext.Current}");
        // await 後面的程式碼作為 Task.Delay 的回撥,
        // 等待 Task.Delay 結束後會由 執行緒池 進行排程執行
        await Task.Delay(100).ConfigureAwait(false);
        // 時間點二:執行緒池執行回撥,這邊已經不存在 SynchronizationContext 了
        Console.WriteLine($"DoSthAsync.SynchronizationContext2: {SynchronizationContext.Current}");
        Console.WriteLine("DoSthAsync: END, CurrentThreadId: {0}", Environment.CurrentManagedThreadId);
    }
}

執行修改後的程式碼:

Test: START, CurrentThreadId: 10
Test.SynchronizationContext1: SingleThreadedSynchronizationContext
DoSthAsync: START, CurrentThreadId: 10
DoSthAsync.SynchronizationContext1: SingleThreadedSynchronizationContext
DoSthAsync.SynchronizationContext2: 
DoSthAsync: END, CurrentThreadId: 6
Test.SynchronizationContext2: SingleThreadedSynchronizationContext
Test: END, CurrentThreadId: 10

ConfigureAwait 方法返回了一個 ConfiguredTaskAwaitable 物件,對原有的 Task 進行了包裝,後續建立 TaskContinuation 的流程裡會走 continueOnCapturedContext: false 的分支。

class Task
{
    public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext)
    {
        return new ConfiguredTaskAwaitable(this, continueOnCapturedContext);
    }
}

為什麼沒有同步上下文也會死鎖

我們的 Web Api 專案中,預設是不存在 SynchronizationContext 的。那為什麼有的同學還會遇到死鎖問題呢,而且主要是高併發的情況下,本地可能沒辦法復現。
這個和 ThreadPool 中的 Starvation Avoidance 機制有關。

DoSthAsync().GetAwaiter().GetResult() 會阻塞執行緒池線。.NET 6之前極端情況導致執行緒池無可用執行緒,導致所謂的“死鎖”。

總結

  1. TaskContinuation:維護回撥和排程回撥。
  2. Awaiter:對 Awaitable 進行封裝,負責與狀態機進行互動。
  3. 狀態機:由編譯器生成,每個 async 方法 有且僅有一個,await 後面的程式碼會被編譯到 狀態機 的 MoveNext 方法中,註冊為 Task 的回撥。
  4. AsyncMethodBuilder:狀態機的重要組成部分,async 方法內外溝通的橋樑,和 async 方法的返回值型別繫結。
  5. 無論何時,都謹慎使用 DoSthAsync().GetAwaiter().GetResult() 這樣的程式碼。

參考資料

https://devblogs.microsoft.com/pfxteam/whats-new-for-parallelism-in-net-4-5-beta/
https://devblogs.microsoft.com/dotnet/configureawait-faq/

相關文章