首頁 > 軟體

C#多執行緒之執行緒繫結ThreadLocal類

2022-06-17 14:02:28

在.Net 4.0的Thread裡,新增了執行緒區域性變數(ThreadLocal)類,可以很方便的實現執行緒專有儲存。

應用場景

執行緒專有儲存應被用於這樣的多執行緒應用:它們經常存取那些邏輯上是全域性的、而物理上是專有於每個執行緒的物件。首先我們看如下這樣一個例子

    string errorMessage;

    void Process()
    {
        bool ret = Run();
        if (!ret && needDebug)
        {
            Console.WriteLine(errorMessage);
        }
    }

    bool Run()
    {
        try
        {
            //…-- do something
            return true;
        }
        catch (Exception e)
        {
            errorMessage = e.Message;
            return false;
        }
    }

這個函數中,Process為主體函數,當它呼叫Run函數失敗後,為調式方便,打出Run函數的錯誤資訊。錯誤資訊採用成員變數errorMessage存放,為了減少Run函數的引數。

這種通過成員變數errorMessage在函數間傳遞資訊的方式在單執行緒程式中可以很好的工作,但是在多執行緒應用時卻往往會發生一些微妙的問題:當兩個執行緒同時執行Run函數時,先執行的會被後執行的執行緒覆蓋,導致輸出了錯誤的後執行的執行緒的偵錯資訊。發生類似資料庫的髒讀錯誤。

解決方案:

最直接的解決方案有兩種:

加鎖:在Process中加鎖,保證沒有兩個執行緒同時存取errorMessage

修改Run函數為bool Run(out string errorMessage)的形式,不通過errorMessage共用資料,使其支援並行操作。

這兩種方式都是有效的,但都有一些不足:加鎖時獲取和釋放互斥體有一個不小的開銷,當共用的資料較多時修改Run函數會導致Run函數變得很難看,並且可能會由於改動較大而導致大規模重構。

針對上述兩種方式的不足,人們提出了執行緒專有儲存的解決方案,使用ThreadLocal類的解決方案如下:

    ThreadLocal<string> errorMessage = new ThreadLocal<string> ();

    void Process()
    {
        bool ret = Run();
        if (!ret && needDebug)
        {
            Console.WriteLine(errorMessage);
        }
    }

    bool Run()
    {
        try
        {
            …- do something
            return true;
        }
        catch (Exception e)
        {
            errorMessage.Value=e.Message;
            return false;
        }
    }

ThreadLocal類在每個執行緒下都分配一個獨立範例副本,每個執行緒都只存取到自己的範例,不會影響其它執行緒,從而解決讀髒資料的問題。

ThreadLocal類也不是什麼新概念,在C++、Java等語言的執行緒庫中都有相關實現,一些語言編譯器實現(如IBM XL FORTRAN)中甚至在語言的層次提供了直接的支援。其實實現的思路很簡單:在ThreadLocal類中有一個雜湊表,根據執行緒ID為key用於儲存每一個執行緒的變數的副本。由於現在沒啥相關資料,並且也是beta版的,我也懶得對.Net中的具體實現和效能進一步分析。

和上面的兩種方式相比,執行緒專有儲存有如下好處:

  • 效率:執行緒專有儲存可實現成無需對執行緒專有資料進行鎖定。例如,通過將errno放入執行緒專有儲存中,每個執行緒都可以可靠地設定和測試該執行緒中的方法的完成狀態,而無需使用複雜的同步協定。這排除了執行緒中共用資料的鎖定開銷,比起獲取和釋放互斥體要更為迅捷。
  • 易於使用:對於應用程式設計師來說,執行緒專有儲存使用起來很簡單,因為系統開發者可以通過資料抽象或宏來使執行緒專有儲存的使用在原始碼級完全透明化。

但也存在如下缺點:

  • 它鼓勵了(執行緒安全的)全域性變數的使用:許多應用不要求多個執行緒通過公用存取點來存取執行緒專有的資料。如果是這樣,資料的儲存應使只有擁有該資料的執行緒可對它進行存取。
  • 它隱藏了系統的結構:執行緒專有儲存的使用隱藏了應用中的物件之間的關係,可能會導致應用更難被理解。

適用性

應用有以下特性時可使用執行緒專有儲存:

  • 應用最初的編寫假定了單執行緒控制,並正在被移植到多執行緒環境,而又不能改變現有API
  • 應用含有多個佔先式執行緒控制,可以任意的排程順序並行執行;
  • 每個執行緒控制呼叫一系列方法,這些方法共用只對該執行緒來說是公用的資料;
  • 在每個執行緒中被物件共用的資料必須通過一個全域性可見的存取點來存取;
  • 存取點"邏輯地"與其他執行緒共用,但在"物理上" 對於每個執行緒卻是唯一的;
  • 資料在方法間隱式地傳遞,而不是經由引數顯式地傳遞。

理解上面描述的特性對於使用(或不使用)執行緒專有儲存模式來說是至關緊要的。例如,UNIX errno變數是一個資料例子:(1)邏輯上全域性,但是物理上執行緒專有,以及(2)在方法間隱式地傳遞。

當應用有以下特性時,不要使用執行緒專有儲存模式:

  • 多個執行緒為單個任務協同工作,該任務需要並行存取共用資料。
    例如,多執行緒應用可以對在記憶體中的資料庫並行地進行讀寫。在這樣的情況下,執行緒必須共用不是執行緒專有的記錄和表。如果使用執行緒專有儲存來儲存此資料庫,執行緒就不能共用這些資料。因而,對資料庫記錄的存取必須通過同步原語(例如,互斥體)來控制,以使執行緒能在共用資料上共同作業。
  • 維護物理和邏輯上都分離的資料要更為直觀和高效。
    例如,通過將資料作為引數顯式地傳遞給所有方法,有可能使執行緒存取僅在每個執行緒中可見的資料。在這樣的情況下,執行緒專有儲存模式有可能是不必要的。

到此這篇關於C#執行緒繫結ThreadLocal類的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com