當前位置: 妍妍網 > 碼農

一文讀懂 .NET 中的 ThreadLocal 和 AsyncLocal

2024-04-18碼農

歡迎來到 Dotnet 工具箱 !在這裏,你可以發現各種令人驚喜的開源計畫!

本篇文章,我們主要介紹一下 C# 中關於異步操作的一個重要知識點,ThreadLocal 和 AsyncLocal。


基本概念

ThreadLocal 用於在多執行緒環境中建立執行緒局部變量,可以讓每個執行緒獨立地存取自己的變量副本,互不影響。

而 AsyncLocal 是 ThreadLocal 的異步版本,專門用於異步編程場景,在異步操作中它可以正確處理上下文切換。


多執行緒的數據問題

接下來,我們透過一個例子,看一下它們是如何使用的?以及它們之間的區別。

staticstringvalue;
staticasync Task Main(string[] args)
{
_ = Print("James");
_ = Print("Bob");
_ = Print("Tony");
Console.ReadKey();
}
staticasync Task Print(string name)
{
await Task.Run(() =>

value = name;
Console.WriteLine($"參數: {name} 當前執行緒: {Thread.CurrentThread.ManagedThreadId} 當前值:{value}");
});
}

在上面的程式碼中,定義了 靜態共享變量 value 值,然後定義一個 Print 打印方法,傳入參數 name,使用 Task.Run 開啟新的執行緒,然後把 靜態變量 value 設定為 name,並使用 Console.Writeline 輸出。

最後,我們執行 3 次 Print 方法,並傳入不同的 name,這樣,3個執行緒同時對共享變量 value 進行賦值,並進行輸出,執行結果會怎麽樣?

程式執行結果,前面是傳入的參數分別是 3個 name,執行緒 id 也不一樣,而共享變量的值,已經出現了混亂(全部為 Tony),在多執行緒環境中,就會出現這樣的情況。

使用 ThreadLocal

這種情況,我們就可以使用 ThreadLocal 來解決,它可以保證每個執行緒的數據都是獨立的,互不影響。

定義一個靜態的 ThreadLocal,然後修改 Print 方法,把 value 換成 threadLocal 的 Value 值,輸出的值也修改為 threadLocal.Value.

staticstringvalue;
staticreadonly ThreadLocal<string> _threadLocal = new ThreadLocal<string>();
staticasync Task Main(string[] args)
{
_ = Print("James");
_ = Print("Bob");
_ = Print("Tony");
Console.ReadKey();
}
staticasync Task Print(string name)
{
await Task.Run(() =>
{
_threadLocal.Value = name;
Console.WriteLine($"參數: {name} 當前執行緒: {Thread.CurrentThread.ManagedThreadId} 當前值:{_threadLocal.Value}");
});
}



再次運行程式檢視輸出結果

可以看到,現在每個執行緒的數據現在都是正確的了,並沒有受到其他執行緒的影響,這就是 ThreadLocal 的功能作用。

接下來,我們修改 Print 方法,使用 Task.Delay 等待 1秒,然後再一次輸出, 如下

staticasync Task Print(string name)
{
await Task.Run(async () =>

_threadLocal.Value = name;
Console.WriteLine($"參數: {name} 當前執行緒: {Thread.CurrentThread.ManagedThreadId} 當前值:{_threadLocal.Value}");
await Task.Delay(1000);
Console.WriteLine($"參數: {name} 當前執行緒: {Thread.CurrentThread.ManagedThreadId} 當前值:{_threadLocal.Value}");
});
}


那麽兩次輸出的結果會一樣嗎?

可以看到,第一次輸出沒有問題,而第二次輸出,數據已經不正確了。

這是因為我們使用 Task 的 await 以後,執行緒就有可能發生切換,當然這是後台的執行緒池來分配的。

執行緒發生改變以後,這裏 ThreadLocal 就不能滿足我們的需求了。

所以,針對這種不是同一個執行緒的異步場景,我們可以使用 AsyncLocal,它是 ThreadLocal的異步替代版本。

使用 AsyncLocal

使用也非常簡單,只需要把 ThreadLocal 改成 AsyncLocal 即可。

然後我們修改 Print 方法,同樣修改為 AsyncLocal,如下

staticstringvalue;
staticreadonly ThreadLocal<string> _threadLocal = new ThreadLocal<string>();
staticreadonly AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
staticasync Task Main(string[] args)
{
_ = Print("James");
_ = Print("Bob");
_ = Print("Tony");
Console.ReadKey();
}
staticasync Task Print(string name)
{
await Task.Run(async () =>
{
_asyncLocal.Value = name;
Console.WriteLine($" A {name} 當前執行緒: {Thread.CurrentThread.ManagedThreadId} 當前值:{_asyncLocal.Value}");
await Task.Delay(1000);
Console.WriteLine($" B {name} 當前執行緒: {Thread.CurrentThread.ManagedThreadId} 當前值:{_asyncLocal.Value}");
});
}






可以看到,就算執行緒發生了改變,兩次輸出的結果都是一樣的,使用 AsyncLocal 解決了問題。

所以,在異步編程中,推薦使用 AsyncLocal,除非你能保證它一直使用的是同一個執行緒,那麽可以使用 ThreadLocal。

AsyncLocal 的工作原理

為什麽 AsyncLocal 在不同的執行緒中,也能存取到相同的數據呢?

這裏就要提到執行上下文 (ExecutionContext), 我們知道,使用 Task 以後,執行緒池會在後台自動分配空閑的執行緒來執行。

當程式建立 Task 的時候,它會拿到上一個 Task 的執行上下文,然後保存在當前 Task 的內部變量中。

在新的執行緒上執行當前這個 Task 的時候,首先會線上程上恢復執行上下文,再執行 Task。

所以,Task 在哪個執行緒是執行是不重要的,因為執行上下文是一直流動傳遞的,所以可以存取到一致的數據。



.NET 異步編程

當你渴望成為.NET架構師時,深入理解異步編程是至關重要的。如果你想更深入地了解相關內容,可以關註下方提供的.NET架構師課程,希望這對你有所幫助~

分享

點收藏

點點贊

點在看