多線程共享變量和 AsyncLocal

>>返回《C# 併發編程》html

1. 簡介

  • 普通共享變量:
    • 在某個類上用靜態屬性的方式便可。
  • 多線程共享變量
    • 但願能將這個變量的共享範圍縮小到單個線程
    • 無關係的B線程沒法訪問到A線程的值;

[ThreadStatic]特性、ThreadLocal<T>CallContextAsyncLocal<T> 都具有這個特性。編程

例子:多線程

因爲 .NET Core 再也不實現 CallContext,因此下列代碼只能在 .NET Framework 中執行併發

class Program
{
    //對照
    private static string _normalStatic;

    [ThreadStatic]
    private static string _threadStatic;
    private static ThreadLocal<string> _threadLocal = new ThreadLocal<string>();
    private static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
    static void Main(string[] args)
    {
        Parallel.For(0, 4, _ =>
        {
            var threadId = Thread.CurrentThread.ManagedThreadId;

            var value = $"這是來自線程{threadId}的數據";
            
            _normalStatic = value;
            _threadStatic = value;
            CallContext.SetData("value", value);
            _threadLocal.Value = value;
            _asyncLocal.Value = value;
            
            Console.WriteLine($"Use Normal;                Thread:{threadId}; Value:{_normalStatic}");
            Console.WriteLine($"Use ThreadStaticAttribute; Thread:{threadId}; Value:{_threadStatic}");
            Console.WriteLine($"Use CallContext;           Thread:{threadId}; Value:{CallContext.GetData("value")}");
            Console.WriteLine($"Use ThreadLocal;           Thread:{threadId}; Value:{_threadLocal.Value}");
            Console.WriteLine($"Use AsyncLocal;            Thread:{threadId}; Value:{_asyncLocal.Value}");
        });

        Console.Read();
    }
}

輸出:異步

Use Normal;                Thread:15; Value:10
Use [ThreadStatic];        Thread:15; Value:15
Use Normal;                Thread:10; Value:10
Use Normal;                Thread:8; Value:10
Use [ThreadStatic];        Thread:8; Value:8
Use CallContext;           Thread:8; Value:8
Use [ThreadStatic];        Thread:10; Value:10
Use CallContext;           Thread:10; Value:10
Use CallContext;           Thread:15; Value:15
Use ThreadLocal;           Thread:15; Value:15
Use ThreadLocal;           Thread:8; Value:8
Use AsyncLocal;            Thread:8; Value:8
Use ThreadLocal;           Thread:10; Value:10
Use AsyncLocal;            Thread:10; Value:10
Use AsyncLocal;            Thread:15; Value:15

結論:async

  • Normal 爲對照組
  • Nomal 的 Thread 與 Value 值不一樣,由於讀到了其餘線程修改的值
  • 其餘的類型,存儲的值,在 Parallel 啓動的線程間是隔離的

2. 異步下的共享變量

平常開發過程當中,咱們常常遇到異步的場景。性能

異步可能會致使代碼執行線程的切換。測試

例如:線程

測試:[ThreadStatic]特性、ThreadLocal<T>AsyncLocal<T> ,三種共享變量被異步代碼賦值後的表現。code

class Program
{
    [ThreadStatic]
    private static string _threadStatic;
    private static ThreadLocal<string> _threadLocal = new ThreadLocal<string>();
    private static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
    static void Main(string[] args)
    {
        _threadStatic = "set";
        _threadLocal.Value = "set";
        _asyncLocal.Value = "set";
        PrintValuesInAnotherThread();
        Console.ReadKey();
    }

    private static void PrintValuesInAnotherThread()
    {
        Task.Run(() =>
        {
            Console.WriteLine($"ThreadStatic: {_threadStatic}");
            Console.WriteLine($"ThreadLocal: {_threadLocal.Value}");
            Console.WriteLine($"AsyncLocal: {_asyncLocal.Value}");
        });
    }
}

輸出:

ThreadStatic: 
ThreadLocal: 
AsyncLocal: set

結論:

在異步發生後,線程被切換,只有 AsyncLocal 還可以保留原來的值.

  • CallContext 也能夠實現這個需求,但 .Net Core 沒有被實現,這裏就不過多說明。

咱們總結一下這些變量的表現:

實現方式 DotNetFx DotNetCore 是否支持數據向輔助線程的
[ThreadStatic]
ThreadLocal
CallContext.SetData(string name, object data) 僅當參數 data 對應的類型實現了 ILogicalThreadAffinative 接口時支持
CallContext.LogicalSetData(string name, object data)
AsyncLocal

輔助線程: 用於處理後臺任務,用戶沒必要等待就能夠繼續使用應用程序,好比線程池線程。

注意:

  • [ThreadStatic]特性、ThreadLocal<T> 最好不要用在線程池線程
    • 線程池線程是可重用的,線程不會銷燬,當線程被重用時,以前使用保存的值依然存在,可能形成影響
  • 使用 AsyncLocal<T> 能夠用在線程池線程
    • 線程使用後迴歸線程池, AsyncLocal<T> 的狀態會被清除,沒法訪問以前的值
  • new Task(...) 默認不是新建一個線程,而是使用線程池線程

3. 解析 AsyncLocal

  • AsyncLocal<T>Value 屬性的真正的數據存取是經過 ExecutionContextinternal 的方法 GetLocalValueSetLocalValue 將數據存到 當前ExecutionContext 上的 m_localValues 字段上
    • ExecutionContext 會根據執行環境進行流動,詳見 《ExecutionContext(執行上下文)綜述》
    • 簡單描述就是,線程發生切換的時候, ExecutionContext 會在前一個線程中被捕獲,流向下一個線程,它所保存的數據也就隨之流動了
      • 在全部會發生線程切換的地方,基礎類庫(BCL) 都爲咱們封裝好了對 ExecutionContext 的捕獲
      • 例如:
        • new Thread(...).Start()
        • new Task(...).Start()
        • Task.Run(...)
        • ThreadPool.QueueUserWorkItem(...)
        • await 語法糖
  • m_localValues 類型是 IAsyncLocalValueMap

3.1. IAsyncLocalValueMap 的實現

如下爲基礎設施提供的實現:

類型 元素個數
EmptyAsyncLocalValueMap 0
OneElementAsyncLocalValueMap 1
TwoElementAsyncLocalValueMap 2
ThreeElementAsyncLocalValueMap 3
MultiElementAsyncLocalValueMap 4 ~ 16
ManyElementAsyncLocalValueMap > 16

隨着 ExecutionContext 所關聯的 AsyncLocal 數量的增長, IAsyncLocalValueMap實現將會在 ExecutionContextSetLocalValue 方法中被不斷替換

  • 查詢的時間複雜度和空間複雜度依次遞增

3.2. 結論

  • AsyncLocal 類型存儲數據,是在本身線程的 ExecutionContext
  • ExecutionContext 的實例會隨着異步或者多線程的啓動而被流向執行後續代碼的其餘線程,保證了啓動異步的線程存儲的數據能夠被訪問到
  • 數據存到 IAsyncLocalValueMap 類型的變量中,此變量會根據存儲的 AsyncLocal 變量個數而切換實現
    • 支持存儲量越大的實現類型,性能越

參考資料
《淺析 .NET 中 AsyncLocal 的實現原理》 --- 黑洞視界

相關文章
相關標籤/搜索