" 返回《C 併發編程》" "1. 簡介" "2. 非同步下的共用變數" "3. 解析 AsyncLocal" "3.1. IAsyncLocalValueMap 的實現" "3.2. 結論" 1. 簡介 + 普通 共用變數: + 在某個類上用靜態屬性的方式即可。 + 多線程 共用變數 + 希望能將這 ...
1. 簡介
- 普通共用變數:
- 在某個類上用靜態屬性的方式即可。
- 多線程共用變數
- 希望能將這個變數的共用範圍縮小到單個線程內
- 無關係的B線程無法訪問到A線程的值;
[ThreadStatic]
特性、ThreadLocal<T>
、CallContext
、AsyncLocal<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 屬性的真正的數據存取是通過 ExecutionContext 的internal
的方法GetLocalValue
和SetLocalValue
將數據存到 當前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 的實現將會在 ExecutionContext
的 SetLocalValue
方法中被不斷替換。
- 查詢的時間複雜度和空間複雜度依次遞增
3.2. 結論
AsyncLocal
類型存儲數據,是在自己線程的 ExecutionContext 中- ExecutionContext 的實例會隨著非同步或者多線程的啟動而被流向執行後續代碼的其他線程,保證了啟動非同步的線程存儲的數據可以被訪問到
- 數據存到
IAsyncLocalValueMap
類型的變數中,此變數會根據存儲的AsyncLocal
變數個數而切換實現- 支持存儲量越大的實現類型,性能越差
參考資料:
《淺析 .NET 中 AsyncLocal 的實現原理》 --- 黑洞視界