淺談AsyncLocal,咱們應該知道的那些事兒

ThreadLocal相信不少童鞋用過,但AsyncLocal具體使用包括我在內的一大部分童鞋應該徹底沒怎麼使用過。
緩存


AsyncLocal一樣出如今.NET Framework 4.6+(包括4.6),固然在.NET Core中沒有版本限制即CoreCLR,對此類官方所給的解釋是:將本地環境數據傳遞到異步控制流,例如異步方法
安全


又例如緩存WCF通訊通道,可使用AsyncLocal而不是.NET Framework或CoreCLR所提供的ThreadLocalapp


官方概念解釋在咱們初次聽來好像仍是有點抽象,不打緊,接下來咱們經過實際例子來進行詳細說明和解釋
框架


AsyncLocal和ThreadLocal區別異步


首先咱們先看以下例子,而後再分析兩者和什麼有關係async

private static readonly ThreadLocal<string> threadLocal = new ThreadLocal<string>();
        
private static readonly AsyncLocal<string> asyncLocal = new AsyncLocal<string>();

static async Task Main(string[] args)
{
    threadLocal.Value = "threadLocal";
    asyncLocal.Value = "asyncLocal";

    await Task.Yield();

    Console.WriteLine("After await: " + threadLocal.Value);

    Console.WriteLine("After await: " + asyncLocal.Value);

    Task.Run(() => Console.WriteLine("Inside child task: " + threadLocal.Value)).Wait();

    Task.Run(() => Console.WriteLine("Inside child task: " + asyncLocal.Value)).Wait();

    Console.ReadLine();
}


猜猜如上將會打印出什麼結果呢?ide

圖片


爲什麼ThreadLocal所打印的值爲空值呢?咱們不是設置了值嗎?此時咱們將要從執行環境開始提及
源碼分析


若徹底理解ExecutionContext與SynchronizationContext兩者概念和關係,理論上來說則可解答出上述問題,這裏咱們簡單敘述下,更詳細介紹請查閱相關資料自行了解ui


ExecutionContext俗稱「執行上下文」,也就是說和「環境」信息相關,這也就意味着它存儲着和咱們當前程序所執行的環境相關的數據,這類環境信息數據存儲在ThreadStatic或ThreadLocal中,換句話說ThreadLocal和特定線程相關spa


上述咱們討論的是相同環境或上下文中,如果不一樣上下文即不一樣線程中,那狀況又該如何呢?


在異步操做中,在某一個線程中啓動操做,但卻在另外一線程中完成,此時咱們將不能利用ThreadLocal來存儲數據,因線程切換所需存儲數據,咱們能夠稱之爲環境「流動」


對於邏輯控制流,咱們指望的是執行環境相關數據能同控制流一塊兒流動,以便能讓執行環境相關數據能從一個線程移動到另一個線程,ExecutionContext的做用就在於此。SynchronizationContext是一種抽象,好比Windows窗體則提供了WindowsFormSynchronizationContext上下文等等


SynchronizationContext做爲ExecutionContext執行環境的一部分

ExecutionContext是當前執行環境,而SynchronizationContext則是針對不一樣框架或UI的抽象

咱們可經過SynchronizationContext.Current獲得當前執行環境信息。


到這裏想必咱們已經明白基於特定線程的ThreadLocal在當前線程設置值後,但await卻不在當前線程,因此打印值爲空,若將上述第一個await去除,則可打印出設置值,而AsyncLocal倒是和執行環境相關,也就是說與線程和調用堆棧有關,並不針對特定線程,它是流動的。


AsyncLocal原理初步分析


首先咱們經過一個簡單的例子來演示AsyncLocal類中值變化過程,咱們能從表面上可得出的結論,而後最終結合源碼進行進一步分析

private static readonly AsyncLocal<string> asyncLocal = new AsyncLocal<string>();

static async Task Main(string[] args)
{
    asyncLocal.Value = "asyncLocal";

    Task.Run(() =>
    {
      asyncLocal.Value = "inside child task asyncLocal";

      Console.WriteLine($"Inside child task: {asyncLocal.Value}");

    }).Wait();

    Console.WriteLine($"after await:{asyncLocal.Value}");

    Console.ReadLine();
}


圖片


由上打印咱們可看出,在Task方法內部將其值進行了修改並打印出修改事後的結果,在Task結束後,最終打印的倒是初始值。


在Task方法內部修改其值,但在任務結束後仍爲初始值,這是一種「寫時複製」行爲,AsyncLocal內部作了兩步操做

進行AsyncLocal實例的拷貝副本,但這是淺複製行爲而非深複製


在設置新的值以前完成複製操做


接下來咱們再經過一個層層調用例子並深刻分析

private static readonly AsyncLocal<string> asyncLocal = new AsyncLocal<string>();

static async Task Main(string[] args)
{
    Demo1().GetAwaiter().GetResult();

    Console.ReadLine();
}

static async Task Demo1()
{
    await Demo2();
    Console.WriteLine($"inside the method of demo1:{asyncLocal.Value}");
}

static async Task Demo2()
{
    SetValue();
    Console.WriteLine($"inside the method of demo2:{asyncLocal.Value}");
}

static void SetValue()
{
    asyncLocal.Value = "initial value";
}



咱們看到此時在Demo1方法內部打印值爲空,由於在Demo2方法內部並未使用異步,因此能打印出所設置的值,這說明以下問題


每次進行實際的aysnc/await後,都會啓動一個新的異步上下文,而且該上下文與父異步上下文徹底隔離且獨立,換句話說,在異步方法內,可查詢本身所屬AsyncLocal<T>,以便能確保不會污染父異步上下文,由於所作更改徹底是針對當前異步上下文的本地內容


至於爲什麼在Demo1方法內部打印爲空,想必咱們已經很清晰,當async方法返回時,返回的是父異步上下文,此時將看不到任何子異步上下文所執行的修改。


AsyncLocal原理源碼分析


咱們來到AsyncLocal類,經過屬性Value設置值,內部經過調用ExecutionContext類中的SetLocalValue方法進行設置,源碼以下:

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

    IAsyncLocal[]? newChangeNotifications = null;
    IAsyncLocalValueMap newValues;
    bool isFlowSuppressed = false;
    if (current != null)
    {
        isFlowSuppressed = current.m_isFlowSuppressed;
        newValues = current.m_localValues.Set(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);
        newChangeNotifications = current.m_localChangeNotifications;
    }
    else
    {
        newValues = AsyncLocalValueMap.Create(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);
    }

    if (needChangeNotifications)
    {
        if (hadPreviousValue)
        {
          Debug.Assert(newChangeNotifications != null);
          Debug.Assert(Array.IndexOf(newChangeNotifications, local) >= 0);
        }
        else if (newChangeNotifications == null)
        {
          newChangeNotifications = new IAsyncLocal[1] { local };
        }
        else
        {
          int newNotificationIndex = newChangeNotifications.Length;
          Array.Resize(ref newChangeNotifications, newNotificationIndex + 1);
          newChangeNotifications[newNotificationIndex] = local;
        }
    }

    Thread.CurrentThread._executionContext =
      (!isFlowSuppressed && AsyncLocalValueMap.IsEmpty(newValues)) ?
      null : 
      new ExecutionContext(newValues, newChangeNotifications, isFlowSuppressed);

    if (needChangeNotifications)
    {
        local.OnValueChanged(previousValue, newValue, contextChanged: false);
    }
}


當首次設置值時,咱們經過Thread.CurrentThread.ExecutionContext,獲取其屬性將爲空,經過AsyncLocalValueMap.Create建立一個AsyncLocal實例並設置值


同時咱們也能夠看到,若在同一執行環境中,當前最新設置值與以前所設置值相同,此時將不會是覆蓋,而是直接返回。


咱們直接來到最後以下幾行代碼:

Thread.CurrentThread._executionContext =
      (!isFlowSuppressed && AsyncLocalValueMap.IsEmpty(newValues)) ?
      null : 
      new ExecutionContext(newValues, newChangeNotifications, isFlowSuppressed);

若默認使用Task默認線程池調度,即便線程池重用線程,其執行環境上下文也會不一樣,如此可說明將更能保證不會將線程數據泄露到另一個線程中,也就是說在重用線程時,但將會保證異步本地實例會按照預期進行GC(我的覺得,理論上狀況應該是這樣,這樣也能保證AsyncLocal是安全的)。


至於其餘關於如何進行值更改後事件通知,這裏就再也不額外展開敘述


因爲AsyncLocal使用淺拷貝,咱們應保證存儲的數據類型不可變,若要修改AsyncLocal<T>實例值,必須保證異步上下文隔離且相互不會影響。


到這裏咱們已徹底清楚,AsyncLocal是針對異步控制流的良好支持,且數據可流動,當前線程AsyncLocal實例所存儲的數據可流動到異步任務控制流中的默認任務調度線程池的線程中


固然咱們也能夠調用以下執行環境上下文中的抑制流動方法來禁用數據流動

private static readonly AsyncLocal<string> asyncLocal = new AsyncLocal<string>();

static async Task Main(string[] args)
{
    asyncLocal.Value = "asyncLocal";

    using (ExecutionContext.SuppressFlow())
    {
      Task.Run(() =>
      {
        Console.WriteLine($"Inside child task: {asyncLocal.Value}");

      }).Wait();
    }

    Console.WriteLine($"after await:{asyncLocal.Value}");

    Console.ReadLine();
}


此時在其任務內部打印的值將爲空。最後,咱們再來對AsyncLocal作一個最終總結

相關文章
相關標籤/搜索