關於C#多線程的文章,大部分都在討論線程的開始與停止或者是多線程同步問題。多線程同步就是在不同線程中訪問同一個變量或共享資源,眾所周知在不使用線程同步的機制下,由於競爭的存在會使某些線程產生臟讀或者是覆蓋其它線程已寫入的值(各種混亂)。
而另外一種情況就是多線程時我們想讓每個線程所訪問的變量只屬於各自線程自身所有,這就是所謂的線程本地變量。
線程本地變量不是用於解決共享變量的問題的,不是為了協調線程同步而存在,而是為了方便每個線程處理自己的狀態而引入的一個機制,理解這點對正確使用線程本來變量至關重要,通過線程本地變量存取的數據,總是與當前線程相關。
本文重點介紹幾種線程本地變量的存儲方式,並簡單介紹一下線程並發訪問各自解決方案。
多線程同步
public class Test { private static object _locker = new object(); public void TryTwoThread() { var b = new Bag(); Action localAct = () => { for (int i = 0; i < 10; i++) { lock(_locker) { ++b.AppleNum; Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - {b.AppleNum}"); } Thread.Sleep(100); } }; Parallel.Invoke(localAct, localAct); } }
最容易的方法就是給共享變量的訪問加個鎖來解決並發問題,當然如果在分布式系統中可以使用分布式鎖。上例執行結果如下圖:
線程本地變量
下面介紹NET下三種線程本地存儲(Thread-Local Storage)方法:ThreadStatic, LocalDataStoreSlot 和 ThreadLocal<T>
1. 使用ThreadStatic特性
線程相關的靜態字段,其做法是將成員變量聲明為static
並打上[ThreadStatic]
這個標記。ThreadStatic特性是最簡單的TLS使用,且只支持靜態字段,只需要在字段上標記這個特性就可以了
//TLS中的str變量 [ThreadStatic] private static string str = "hehe"; static void Main() { //另一個線程只會修改自己TLS中的str變量 Thread th = new Thread(() => { str = "Mgen"; Display(); }); th.Start(); th.Join(); Display(); } static void Display() { Console.WriteLine("{0} {1}", Thread.CurrentThread.ManagedThreadId, str); }
運行結果:
3 Mgen 1 hehe
可以看到,str靜態字段在兩個線程中都是獨立存儲的,互相不會被修改。
2. 數據槽
顯然ThreadStatic特性只支持靜態字段太受限制了,.NET線程類型中的LocalDataStoreSlot提供更好的TLS支持,但是性能不如上面介紹的ThreadStatic方法。注意:LocalDataStoreSlot有命名類型和非命名類型區分。
我們先來看看命名的LocalDataStoreSlot類型,可以通過Thread.AllocateNamedDataSlot來分配一個命名的空間,通過Thread.FreeNamedDataSlot來銷毀一個命名的空間。
把線程相關的數據存儲在LocalDataStoreSlot對象中,空間數據的獲取和設置則通過Thread類型的GetData方法和SetData方法。
static void Main() { //創建Slot LocalDataStoreSlot slot = Thread.AllocateNamedDataSlot("slot"); //設置TLS中的值 Thread.SetData(slot, "hehe"); //修改TLS的線程 Thread th = new Thread(() => { Thread.SetData(slot, "Mgen"); Display(); }); th.Start(); th.Join(); Display(); //清除Slot Thread.FreeNamedDataSlot("slot"); } //顯示TLS中Slot值 static void Display() { LocalDataStoreSlot dataslot = Thread.GetNamedDataSlot("slot"); Console.WriteLine("{0} {1}", Thread.CurrentThread.ManagedThreadId, Thread.GetData(dataslot)); }
運行結果:
3 Mgen 1 hehe
在多組件的情況下,用不同名稱區分數據槽很有用。但如果不小心給不同組件起了相同的名字,則會導致數據污染。
線程同樣支持未命名的LocalDataStoreSlot,未命名的LocalDataStoreSlot不需要手動清除,分配則需要Thread.AllocateDataSlot方法。
注意由於未命名的LocalDataStoreSlot沒有名稱,因此無法使用Thread.GetNamedDataSlot方法,只能在多個線程中引用同一個LocalDataStoreSlot才可以對TLS空間進行操作,
將上面的命名的LocalDataStoreSlot代碼改成未命名的LocalDataStoreSlot執行:
//靜態LocalDataStoreSlot變量 private static LocalDataStoreSlot slot; static void Main() { //創建Slot slot = Thread.AllocateDataSlot(); //設置TLS中的值 Thread.SetData(slot, "hehe"); //修改TLS的線程 Thread th = new Thread(() => { Thread.SetData(slot, "Mgen"); Display(); }); th.Start(); th.Join(); Display(); } //顯示TLS中Slot值 static void Display() { Console.WriteLine("{0} {1}", Thread.CurrentThread.ManagedThreadId, Thread.GetData(slot)); }
輸出和上面的類似。
數據槽的性能較低,微軟也不推薦使用,而且不是強類型的,用起來也不太方便。另外LocalDataStoreSlot不可能有默認值,因為初始化只能構造一個空間
3. .NET 4.0的ThreadLocal<T>類型
在.NET Framework 4.0以后新增了一種泛型化的本地變量存儲機制 - ThreadLocal<T>
。他的出現更大的簡化了TLS的操作,下面的例子也是在之前例子基礎上修改的。
對比之前代碼就很好理解ThreadLocal<T>
的使用,ThreadLocal<T>
的構造函數接收一個lambda用於線程本地變量的延遲初始化,通過Value屬性可以訪問本地變量的值。
IsValueCreated可以判斷本地變量是否已經創建。
private static ThreadLocal<string> local;
public ThreadLocal<string> BagLocal;
static void Main() { //創建ThreadLocal並提供默認值 local = new ThreadLocal<string>(() => "hehe", true);
BagLocal = local; //修改TLS的線程 Thread th = new Thread(() => { local.Value = "Mgen"; //通過Value屬性訪問 Display(); }); th.Start(); th.Join(); Display(); } //顯示TLS中數據值 static void Display() { Console.WriteLine("{0} {1}", Thread.CurrentThread.ManagedThreadId, local.Value); }
運行結果:
3 Mgen 1 hehe
另外如果在初始化ThreadLocal<T>
時,將其trackAllValues設置為true,則可以在使用ThreadLocal<T>
的線程外部訪問線程本地變量中所存儲的值。如在測試代碼中:
public void TryTwoThread() { var worker = new Worker(); Parallel.Invoke(worker.PutTenApple, worker.PutTenApple); //可以使用Values在線程外訪問所有線程本地變量(需要ThreadLocal初始化時將trackAllValues設為true) foreach (var tval in worker.BagLocal.Values) { Console.WriteLine(tval.AppleNum); } }