多線程共享變量和 AsyncLocal


>>返回《C# 並發編程》

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

結論:

  • Normal 為對照組
  • Nomal 的 Thread 與 Value 值不同,因為讀到了其他線程修改的值
  • 其他的類型,存儲的值,在 Parallel 啟動的線程間是隔離的

2. 異步下的共享變量

日常開發過程中,我們經常遇到異步的場景。

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

例如:

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

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 的實現原理》 --- 黑洞視界


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM