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

前言

本文爲系列博客

  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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章