細聊C# AsyncLocal如何在異步間進行數據流轉

前言

    在異步編程中,處理異步操作之間的數據流轉是一個比較常用的操作。C#異步編程提供了一個強大的工具來解決這個問題,那就是AsyncLocal。它是一個線程本地存儲的機制,可以在異步操作之間傳遞數據。它爲我們提供了一種簡單而可靠的方式來共享數據,而不必擔心線程切換或異步上下文的變化。本文我們將探究AsyncLocal的原理和用法,並進行相關源碼解析。探討它如何在異步操作之間實現數據的流轉,以及它是如何在底層工作的。

使用方式

上面我們提到了AsyncLocal可以在異步操作間傳遞數據,我們在之前的文章<研究c#異步操作async await狀態機的總結>一文中提到過異步操作會涉及到線程切換的問題,接下來通過Task來模擬一個簡單異步示例,來看一下它的工作方式是什麼樣的,以便加深對它的理解,先看一下示例

AsyncLocal<Person> context = new AsyncLocal<Person>();
context.Value = new Person { Id = 1, Name = "張三" };
Console.WriteLine($"Main之前:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
await Task.Run(() =>
{
    Console.WriteLine($"Task1之前:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
    context.Value.Name = "李四";
    Console.WriteLine($"Task1之後:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
});

await Task.Run(() =>
{
    Console.WriteLine($"Task2之前:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
    context.Value.Name = "王五";
    Console.WriteLine($"Task2之後:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"Main之後:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");

在上面的示例中,我們創建了一個AsyncLocal實例,並賦值了一個Person對象,然後我們創建了兩個Task,分別執行了兩個異步操作,並分別修改了AsyncLocal中的Person對象的值,分別在執行異步之前執行異步過程中和執行異步之後打印值來觀察變化,執行程序輸出結果如下

Main之前:張三,ThreadId=1
Task1之前:張三,ThreadId=4
Task1之後:李四,ThreadId=4
Task2之前:李四,ThreadId=6
Task2之後:王五,ThreadId=6
Main之後:王五,ThreadId=6

從輸出結果來看,雖然我們在異步中修改了AsyncLocalPerson對象的值,並且也發生了線程切換。但是它可以在異步操作之間的數據共享和傳遞,使得我們在異步間進行的數據就和在一個線程裏操作數據一樣,讓我們可以忽略掉其實已經發生了多次線程切換。

探究本質

通過上面的示例,我們發現AsyncLocal確實可以實現異步之間的數據共享和傳遞,那麼它是如何實現的呢?接下來,我們通過先查看AsyncLocal涉及到的相關源碼來探究一下。想弄明白它的流轉問題,需要研究兩個大方向,一個是AsyncLocal的本身實現,一個是AsyncLocal的流轉涉及到的異步或者多線程相關這裏涉及到的主要是Task線程池裏的相關實現。由於異步相關涉及到了一整個體系,所以但看某一點的時候可能不太容易理解,我們先從AsyncLocal本身入手,然後從Task入手,最後從線程池入手,逐步探究AsyncLocal如何進行數據流轉的。但是仍然希望能在閱讀本文之前先了解一下設計到該話題的相關文章,先對整體有一個整體的把握

AsyncLocal

雖然強烈建議先看一下上面推薦的文章,但是在這裏我們還是簡單介紹一下AsyncLocal的實現,所以這裏我們簡單介紹一下,方便大家能直觀的看到。其實涉及到的比較簡單,就是看一下AsyncLocal裏涉及到關於Value的操作即可[點擊查看AsyncLocal.Value源碼👈]

public sealed class AsyncLocal<T> : IAsyncLocal
{
    [MaybeNull]
    public T Value
    {
        get
        {
            object? value = ExecutionContext.GetLocalValue(this);
            if (typeof(T).IsValueType && value is null)
            {
                return default;
            }

            return (T)value!;
        }
        set
        {
            ExecutionContext.SetLocalValue(this, value, _valueChangedHandler is not null);
        }
    }
}

通過上面的源碼可以看到AsyncLocal的Value屬性的能力來自於ExecutionContext,也可理解爲AsyncLocal是對ExecutionContext能力的包裝。

在C#中,ExecutionContext是用於多線程和異步編程的類,用於保存和還原線程的執行狀態。它的主要功能是確保在線程切換時,狀態得以保留和恢復,以便線程能夠在正確的上下文中繼續執行。這有助於管理線程的數據、狀態以及異步任務的正確執行。

所以我們可以繼續簡單的看一下ExecutionContext中關於GetLocalValue方法和SetLocalValue方法的大致實現,這裏我們不在進行全部代碼展示,只展示核心實現[點擊查看ExecutionContext.LocalValue源碼👈]

public sealed class ExecutionContext : IDisposable, ISerializable
{
    private readonly IAsyncLocalValueMap? m_localValues;

    private ExecutionContext(
        IAsyncLocalValueMap localValues,)
    {
        m_localValues = localValues;
    }

    //獲取值的方法
    internal static object? GetLocalValue(IAsyncLocal local)
    {
        //捕獲當前線程的執行上下文
        ExecutionContext? current = Thread.CurrentThread._executionContext;
        if (current == null)
        {
            return null;
        }
        //在執行上下文中獲取值
        current.m_localValues.TryGetValue(local, out object? value);
        return value;
    }

    //設置值的方法
    internal static void SetLocalValue(IAsyncLocal local, object? newValue, bool needChangeNotifications)
    {
        ExecutionContext? current = Thread.CurrentThread._executionContext;
        //判斷設置的心值和舊值是否相同
        object? previousValue = null;
        bool hadPreviousValue = false;
        if (current != null)
        {
            hadPreviousValue = current.m_localValues.TryGetValue(local, out previousValue);
        }
        //相同的話不在進行設置直接返回
        if (previousValue == newValue)
        {
            return;
        }

        if (current != null)
        {
            //設置新值
            newValues = current.m_localValues.Set(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);
        }
        else
        {
            //如果沒有使用過先初始化在存儲
            newValues = AsyncLocalValueMap.Create(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);
        } 
        //給當前線程執行上下文賦值新值
        Thread.CurrentThread._executionContext = (!isFlowSuppressed && AsyncLocalValueMap.IsEmpty(newValues)) ?
            null : new ExecutionContext(newValues, newChangeNotifications, isFlowSuppressed);

    }
}

通過上面的代碼我們可以知道GetLocalValue函數用於從當前線程的執行上下文中獲取異步本地對象的值。通過檢索執行狀態並查找本地值字典,該函數能夠獲取正確的值,實現了上下文數據的提取。SetLocalValue函數用於設置異步本地對象的值。它通過比較新舊值、操作本地值字典,並根據情況創建新的執行上下文,確保了數據正確地傳遞和存儲。而異步操作過程中無非也正是不同線程上下文之間切換的問題。

有關ExecutionContext更詳細的源碼可以仔細閱讀一下,上面開頭提到的黑洞大佬文章地址。

在異步中流轉

上面我們展示了AsyncLocal相關的代碼實現,知道了AsyncLocal本質是對ExecutionContext能力的封裝。每個線程Thread對象都包含了_executionContext類存儲ExecutionContext執行上下問信息。接下來我們就來研究一下AsyncLocal中的數據是如何在異步過程中流轉的。首先我們來大致回顧一下異步編譯之後形成狀態機的執行過程。

IAsyncStateMachine狀態機實例
  ->AsyncTaskMethodBuilder屬性類型AwaitUnsafeOnCompleted方法->
    ->AsyncTaskMethodBuilder<VoidTaskResult>.AwaitUnsafeOnCompleted方法
        判斷是否是以下類型
        ITaskAwaiter
        IConfiguredTaskAwaiter
        IStateMachineBoxAwareAwaiter
            ->Task是類型ITaskAwaiter類型所以調用UnsafeOnCompletedInternal方法
                ->Task.UnsafeSetContinuationForAwait
                    ->判斷交由哪種執行策略執行比如TaskScheduler或ThreadPool

到了Task.UnsafeSetContinuationForAwait方法這一步會涉及到異步代碼如何被調度的問題也就是會被自定義調度策略調度還是被線程池調度等等。我們來看一下這個方法的實現,這個方法的實現代碼,在上面的<研究c#異步操作async await狀態機的總結>一文中也有介紹,咱們簡單看一下這裏面的代碼[點擊查看Task.UnsafeSetContinuationForAwait源碼👈]

internal void UnsafeSetContinuationForAwait(IAsyncStateMachineBox stateMachineBox, bool continueOnCapturedContext)
{
    //是否捕獲同步上下文
    if (continueOnCapturedContext)
    {
        //在異步執行完成後通過同步上下文執行後續結果
        SynchronizationContext? syncCtx = SynchronizationContext.Current;
        if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext))
        {
            var tc = new SynchronizationContextAwaitTaskContinuation(syncCtx, stateMachineBox.MoveNextAction, flowExecutionContext: false);
            if (!AddTaskContinuation(tc, addBeforeOthers: false))
            {
                tc.Run(this, canInlineContinuationTask: false);
            }
            return;
        }
        else
        {
            //選擇執行默認的TaskScheduler還是自定義的Scheduler
            TaskScheduler? scheduler = TaskScheduler.InternalCurrent;
            if (scheduler != null && scheduler != TaskScheduler.Default)
            {
                var tc = new TaskSchedulerAwaitTaskContinuation(scheduler, stateMachineBox.MoveNextAction, flowExecutionContext: false);
                if (!AddTaskContinuation(tc, addBeforeOthers: false))
                {
                    tc.Run(this, canInlineContinuationTask: false);
                }
                return;
            }
        }
    }

    if (!AddTaskContinuation(stateMachineBox, addBeforeOthers: false))
    {
        //兜底的線程池策略
        ThreadPool.UnsafeQueueUserWorkItemInternal(stateMachineBox, preferLocal: true);
    }
}

在許多情況下,特定的代碼需要在特定的線程上執行,例如UI操作需要在UI線程上執行,以避免UI衝突和渲染問題。SynchronizationContext就是爲了解決這樣的問題而引入的。它允許您捕獲和存儲特定線程的上下文,並在需要時將任務切換到正確的線程。

上面的這段源碼是Task執行操作的核心策略,咱們簡單的分析一下這段代碼涉及到的幾個核心的邏輯

  • 首先是continueOnCapturedContext判斷,我們使用task.ConfigureAwait(false)這裏設置的true或false設置的就是continueOnCapturedContext的值,如果爲true則表示當前Task的執行需要切換到當前SynchronizationContext的線程,如果用一段代碼描述默認情況下一步執行的原理可以大致理解爲下面的代碼。
    SynchronizationContext sc = SynchronizationContext.Current;
    ThreadPool.QueueUserWorkItem(_ =>
    {
        try 
        { 
            DoWorker();
        }
        finally 
        { 
            sc.Post(_ => callback(), null); 
        }
    });
    
  • 其次是scheduler != TaskScheduler.Default判斷,如果自定義了TaskScheduler則使用自定義的TaskScheduler執行,否則使用ThreadPool的線程池執行。比如經典問題Task.Factory.StartNew()方法中await前後如果不想切換線程可以只用自定義TaskScheduler的方式只用一個Thread執行所有任務,示例代碼如下所示。
    await Task.Factory.StartNew(async () =>
    {
        while (true)
        {
            Console.WriteLine($"Task之前Current Thread:{Thread.CurrentThread.ManagedThreadId}");
            await Task.Delay(2000);
            Console.WriteLine($"Task之後Current Thread:{Thread.CurrentThread.ManagedThreadId}");
        }
    }, CancellationToken.None, TaskCreationOptions.None, new SingleThreadScheduler());
    
    public class SingleThreadScheduler : TaskScheduler
    {
        private readonly BlockingCollection<Task> _tasks = new BlockingCollection<Task>();
    
        public SingleThreadScheduler()
        {
            var thread = new Thread(() =>
            {
                foreach (var task in _tasks.GetConsumingEnumerable())
                {
                    if (!TryExecuteTask(task))
                    { 
                        _tasks.Add(task);
                    }
                }
            })
            {
                IsBackground = true
            };
            thread.Start();
        }
    
        protected override IEnumerable<Task>? GetScheduledTasks()
        {
            return _tasks;
        }
    
        protected override void QueueTask(Task task)
        {
            _tasks.Add(task);
        }
    
        protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
        {
            return false;
        }
    }
    
  • 最後兜底的策略就是使用ThreadPool線程池去執行異步任務。

好了接下來我們把探索的重心就在線程池的裏,我們知道自從有了Task之後ThreadPool就是和Task關聯起來的,關聯的核心邏輯就是在ThreadPoolWorkQueue的DispatchWorkItem方法中[點擊查看ThreadPoolWorkQueue.DispatchWorkItem源碼👈]

private static void DispatchWorkItem(object workItem, Thread currentThread)
{
    //判斷如果線程池執行的任務是Task任務則執行Task裏的ExecuteFromThreadPool方法
    if (workItem is Task task)
    {
        //傳遞當前的線程池裏的線程
        task.ExecuteFromThreadPool(currentThread);
    }
    else
    {
        Debug.Assert(workItem is IThreadPoolWorkItem);
        Unsafe.As<IThreadPoolWorkItem>(workItem).Execute();
    }
}

通過上面的源碼我們可以看到如果線程池執行的任務是Task任務則執行Task裏的ExecuteFromThreadPool方法裏,從這裏我們也可以看到TaskThreadPool的關聯性。需要注意的是這裏雖然關聯的Task類型但是並非是Task類的實例本身,而是實現了Task類的狀態機類型AsyncStateMachineBox<TStateMachine>,通過跟蹤生成的狀態機代碼我們可以看到,實際添加到線程池的是IAsyncStateMachineBox實例,而AsyncStateMachineBox<TStateMachine>即繼承了Task也實現了IAsyncStateMachineBox接口,由於邏輯較多隻粘貼咱們關注的部分[點擊查看AsyncTaskMethodBuilderT.GetStateMachineBox源碼👈]

private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>(
            ref TStateMachine stateMachine,
            [NotNull] ref Task<TResult>? taskField)
            where TStateMachine : IAsyncStateMachine
{
    //捕獲當前線程上下文
    ExecutionContext? currentContext = ExecutionContext.Capture();

    //創建AsyncStateMachineBox實例
    AsyncStateMachineBox<TStateMachine> box = AsyncMethodBuilderCore.TrackAsyncMethodCompletion ?
    CreateDebugFinalizableAsyncStateMachineBox<TStateMachine>() : new AsyncStateMachineBox<TStateMachine>();
    taskField = box; 
    box.StateMachine = stateMachine;
    //傳遞當前捕獲的ExecutionContext執行上下文
    box.Context = currentContext;

    return box;
}

在上面的方法中我們看到在初始化AsyncStateMachineBox<TStateMachine>實例之前先使用ExecutionContext.Capture()方法捕獲執行上下文傳遞進來,這個時候還不存在被線程池執行一說,所以捕獲的肯定是初始化Task的線程,注意這個時候還沒有執行Task裏的任何邏輯。所以我們關注一下ExecutionContext.Capture()方法的實現[點擊查看EExecutionContext.Capture源碼👈]

public static ExecutionContext? Capture()
{
    //捕獲當前線程的執行上下文
    ExecutionContext? executionContext = Thread.CurrentThread._executionContext;
    if (executionContext == null)
    {
        executionContext = Default;
    }
    //如果設置ExecutionContext.RestoreFlow()則不進行捕獲
    else if (executionContext.m_isFlowSuppressed)
    {
        executionContext = null;
    }

    return executionContext;
}

通過上面的代碼我們看到了ExecutionContext.Capture()就是捕獲當前線程的執行上下文,如果設置了ExecutionContext.RestoreFlow()上面邏輯裏的m_isFlowSuppressed值則爲true這個時候則不進行上下文捕獲。好了我們繼續往下看,上面的GetStateMachineBox方法返回的正是AsyncStateMachineBox<TStateMachine>類實例,它是線程池線程中真正執行的Task實例,我們看一下的定義[點擊查看AsyncStateMachineBox源碼👈]

private class AsyncStateMachineBox<TStateMachine> :
            Task<TResult>, IAsyncStateMachineBox
            where TStateMachine : IAsyncStateMachine
{
}

這裏我們可以看到AsyncStateMachineBox<TStateMachine>類是繼承自Task類也實現了IAsyncStateMachine ,所以上面的ThreadPoolWorkQueue.DispatchWorkItem方法中調用的ExecuteFromThreadPool方法,本質是調用的AsyncStateMachineBox<TStateMachine>.ExecuteFromThreadPool方法,我們看一下它的實現方式[點擊查看AsyncStateMachineBox.ExecuteFromThreadPool源碼👈]

internal sealed override void ExecuteFromThreadPool(Thread threadPoolThread) => MoveNext(threadPoolThread);
public void MoveNext() => MoveNext(threadPoolThread: null);

private void MoveNext(Thread? threadPoolThread)
{
    //獲取之前捕獲的ExecutionContext執行上下文
    ExecutionContext? context = Context;
    if (context == null)
    {
        Debug.Assert(StateMachine != null);
        StateMachine.MoveNext();
    }
    else
    {
        //判斷是否是線程池代碼
        if (threadPoolThread is null)
        {
            ExecutionContext.RunInternal(context, s_callback, this);
        }
        else
        {
            //默認是線程池線程,會走到這裏的邏輯
            ExecutionContext.RunFromThreadPoolDispatchLoop(threadPoolThread, context, s_callback, this);
        }
    }
}

源碼中的s_callback本質是調用狀態機生成的MoveNext方法,也就是在線程池線程裏需要被執行的邏輯,我們看一下它的定義

private static readonly ContextCallback s_callback = ExecutionContextCallback;
private static void ExecutionContextCallback(object? s)
{
    //本質調用的狀態機生成的MoveNext方法
    Unsafe.As<AsyncStateMachineBox<TStateMachine>>(s).StateMachine!.MoveNext();
}

上面的這段代碼可以清楚的看到線程池線程裏執行的邏輯是async await生成的狀態機裏的代碼,完成了多線程執行狀態機邏輯的關聯。

咱們再繼續看AsyncStateMachineBox.MoveNext方法裏的執行邏輯。由於咱們是默認機制所以這段邏輯肯定是在線程池裏的線程執行,所以會執行到ExecutionContext.RunFromThreadPoolDispatchLoop()方法裏,我們看一下它的邏輯[點擊查看ExecutionContext.RunFromThreadPoolDispatchLoop源碼👈]

internal static void RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, object state)
{
    //threadPoolThread是線程池線程,executionContext是Task.CapturedContext捕獲的執行上下文
    if (executionContext != null && !executionContext.m_isDefault)
    {
        //如果線程存在ExecutionContext則把捕獲到的執行上下文賦值給當前線程池線程的執行上下文ExecutionContext
        RestoreChangedContextToThread(threadPoolThread, contextToRestore: executionContext, currentContext: null);
    }

    ExceptionDispatchInfo? edi = null;
    try
    {
        //執行Task裏的邏輯
        callback.Invoke(state);
    }
    catch (Exception ex)
    {
        edi = ExceptionDispatchInfo.Capture(ex);
    }

    //捕獲當前線程池線程
    Thread currentThread = threadPoolThread;
    //獲取當前線程池裏的執行上下文
    ExecutionContext? currentExecutionCtx = currentThread._executionContext;
    currentThread._synchronizationContext = null;
    if (currentExecutionCtx != null)
    {
        //將當前線程池裏的執行上下文清空,方便下次在線程池裏獲取到當前線程處於初始化狀態
        RestoreChangedContextToThread(currentThread, contextToRestore: null, currentExecutionCtx);
    }
    edi?.Throw();
}

internal static void RestoreChangedContextToThread(Thread currentThread, ExecutionContext? contextToRestore, ExecutionContext? currentContext)
{
    //把捕獲到的執行上下文賦值給當前線程池線程的執行上下文ExecutionContext
    currentThread._executionContext = contextToRestore;
    if ((currentContext != null && currentContext.HasChangeNotifications) ||
        (contextToRestore != null && contextToRestore.HasChangeNotifications))
    {
        OnValuesChanged(currentContext, contextToRestore);
    }
}

從上面的ExecutionContext.ExecuteFromThreadPool裏的邏輯我們可以清楚的看到我們想要的結果,由於上面提供了大片的源碼,看起來容易混亂,老規矩我們在這裏總結一下核心邏輯的執行流程

  • 在線程池線程執行當前Task裏的任務之前即AsyncStateMachineBox實例,因爲它就是Task子類。先通過ExecutionContext.Capture()捕獲當前線程的ExecutionContext執行上下文,方便給接下來線程池裏的線程使用。
  • 把上一步裏捕獲到的ExecutionContext執行上下文,填充到在ThreadPool裏得到的線程的執行上下文_executionContext裏,這樣就完成了不同線程之間的執行上下文流轉。
  • 執行完當前Task之後,把當前線程池中捕獲的線程執行上下文給還原掉,也就是上面的RestoreChangedContextToThread(currentThread, contextToRestore: null, currentExecutionCtx)使用null賦值。

通過上面的總結相信大家對執行上下文數據流轉有個很好的理解了。先捕獲當前線程執行上下文,然後把捕獲的執行上下文填充到要執行任務的線程池的線程裏,這樣就完成了不同線程中執行上下文的流轉,執行完Task任務之後把線程池裏線程的執行上下文還原掉方便下次執行的時候是初始化狀態。

一個常見的坑

通過上面的源碼解析我們清楚的瞭解到了AsyncLocal在異步中是如何傳遞的,其實本質也就是在不同的線程裏傳遞。那麼接下來我們看一個大家在使用的過程中容易出錯的地方,還是剛開始的例子,我們改造一下示例代碼,如下所示

AsyncLocal<Person> context = new AsyncLocal<Person>();
context.Value = new Person { Id = 1, Name = "張三" };
Console.WriteLine($"Main之前:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
await Task.Run(() =>
{
    Console.WriteLine($"Task1之前:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
    context.Value = new Person { Id = 2, Name = "李四" };
    Console.WriteLine($"Task1之後:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
});

await Task.Run(() =>
{
    Console.WriteLine($"Task2之前:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
    context.Value = new Person { Id = 3, Name = "王五" };;
    Console.WriteLine($"Task2之後:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"Main之後:{context.Value.Name},ThreadId={Thread.CurrentThread.ManagedThreadId}");

這段代碼的執行結果大家猜到了嗎?不賣關子了,上面的示例代碼執行結果如下所示

Main之前:張三,ThreadId=1
Task1之前:張三,ThreadId=6
Task1之後:李四,ThreadId=6
Task2之前:張三,ThreadId=8
Task2之後:王五,ThreadId=8
Main之後:張三,ThreadId=8

這裏我們可以看到,雖然我們在不同的Task裏改變了AsyncLocal裏的Value值比如改成了李四王五這種,但是執行完Task之後彷彿值又被還原成最初初始化時候的樣子也就是上面說的張三,爲什麼會這個樣子呢?我們來分析一下

  • 1.初始化線程我們叫線程A,線程A.ExecutionContext存儲的是Person { Id = 1, Name = "張三" }內存區域的引用。
  • 2.第一個Task中執行邏輯之前捕獲了線程A.ExecutionContext賦值給再線程池中線程線程B,現在線程A.ExecutionContext線程B.ExecutionContext都指向內存區域Person { Id = 1, Name = "張三" },因爲數據是直接流轉過來的,上面的邏輯裏我們提到過。
  • 3.在接下來的Task裏我們得到線程池線程線程B在這裏我們實例化了一個新的Person { Id = 2, Name = "李四" }實例,此時線程B.ExecutionContext的引用讓指向Person { Id = 2, Name = "李四" }內存區域,線程A.ExecutionContext指向的依然的是Person { Id = 1, Name = "張三" }內存區域。
  • 4.線程B執行完成之後要還原掉執行上下文賦值null,這個時候線程B.ExecutionContext的引用讓指向null,線程A.ExecutionContext指向的依然的是Person { Id = 1, Name = "張三" }內存區域。
  • 5.進入另一個Task之後我們得到線程池線程線程C,接下來線程C重複執行上面的2、3、4步驟。

畫個圖簡單的演示一下,首先是初始化的時候這個時候線程A.ExecutionContext線程B.ExecutionContext都指向內存區域Person { Id = 1, Name = "張三" }如下所示

線程B裏重新實例化了一個新的Person實例,此時的引用指向發生了變化,如下所示

這個時候線程A.ExecutionContext線程B.ExecutionContext已經沒啥關係了,所以你無論怎麼操作線程B.ExecutionContext也和線程A.ExecutionContext沒有任何關係了。

總結

    通過本文我們探究了AsyncLocal中的數據如何在異步之間如何流轉數據的,本質還是在多個線程之間流轉數據。接下來我們大致的總結一下本文的核心內容

  • 首先我們探究了AsyncLocal知道了它是對ExecutionContext執行上下文能力的包裝,每個線程都會包含一個執行上下文,即Thread._executionContext屬性。
  • 當使用異步或者線程池線程執行Task裏的任務之前,即AsyncStateMachineBox實例,因爲它就是Task子類。先通過ExecutionContext.Capture()捕獲當前線程的ExecutionContext執行上下文,方便給接下來線程池裏的線程使用。
  • 然後把上一步裏捕獲到的ExecutionContext執行上下文,填充到在ThreadPool裏得到的線程的執行上下文_executionContext裏,這樣就完成了不同線程之間的執行上下文流轉。
  • 執行完當前Task之後,把當前線程池中捕獲的線程執行上下文給還原掉,也就是上面的RestoreChangedContextToThread(currentThread, contextToRestore: null, currentExecutionCtx)使用null賦值。

也就是先捕獲當前線程執行上下文,然後把捕獲的執行上下文填充到要執行任務的線程池的線程裏,這樣就完成了不同線程中執行上下文的流轉,執行完Task任務之後把線程池裏線程的執行上下文還原掉方便下次執行的時候是初始化狀態。

    分享一下近期通過雷總的演講得到的一點感悟,周邊知識不足,認知體系不全,有時單看某個點確實很難理解。跳出來看全局,補齊體系,可以不深,但一定得有整體認識,回頭再看由淺入深,再由點帶面,有時候更容易貫通。

👇歡迎掃碼關注我的公衆號👇
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章