欧美vvv,亚洲第一成人在线,亚洲成人欧美日韩在线观看,日本猛少妇猛色XXXXX猛叫

新聞資訊


    我們從線程的基礎(chǔ)知識(shí)開始第 ,作為任務(wù)和異步的前身。具體來說,我們展示了如何啟動(dòng)和配置線程,并介紹了線程池、阻塞、旋轉(zhuǎn)和同步上下文等基本概念。我們還介紹了鎖定和線程安全,并演示了最簡(jiǎn)單的信令結(jié)構(gòu) ManualResetEvent 。

    本章繼續(xù)第 關(guān)于線程的主題。在前三節(jié)中,我們將更詳細(xì)地充實(shí)同步、鎖定和線程安全。然后,我們將介紹:

    • 非獨(dú)占鎖定(信號(hào)燈和讀/寫器鎖定)
    • 所有信令結(jié)構(gòu)(自動(dòng)復(fù)位事件、手動(dòng)復(fù)位事件、倒計(jì)時(shí)事件和屏障)
    • Lazy initialization ( Lazy<T> and LazyInitializer )
    • 線程本地存儲(chǔ)(ThreadStaticAttribute 、ThreadLocal<T> 和 GetData / SetData )
    • 定時(shí)器

    線程是一個(gè)如此廣泛的話題,我們已經(jīng)將其他材料放在網(wǎng)上來完成圖片。訪問 ,討論以下更晦澀難懂的主題:

    • Monitor.Wait and Monitor.Pulse 用于專用信令場(chǎng)景
    • 用于微優(yōu)化的非阻塞同步技術(shù)(互鎖,內(nèi)存屏障,易失性)
    • SpinLock 和 SpinWait 適用于高并發(fā)場(chǎng)景

    同步概述

    是協(xié)調(diào)并發(fā)操作以獲得可預(yù)測(cè)結(jié)果的行為。當(dāng)多個(gè)線程訪問相同的數(shù)據(jù)時(shí),同步尤為重要;在這個(gè)地區(qū)擱淺非常容易。

    最簡(jiǎn)單和最有用的同步工具可以說是中描述的延續(xù)和任務(wù)組合器。通過將并發(fā)程序制定為與延續(xù)器和組合器串在一起的異步操作,您可以減少對(duì)鎖定和信令的需求。但是,有時(shí)較低級(jí)別的構(gòu)造仍然會(huì)發(fā)揮作用。

    同步構(gòu)造可分為三類:

    獨(dú)家鎖定

    獨(dú)占鎖定構(gòu)造一次只允許一個(gè)線程執(zhí)行某些活動(dòng)或執(zhí)行一段代碼。它們的主要目的是讓線程訪問共享寫入狀態(tài),而不會(huì)相互干擾。獨(dú)占鎖定結(jié)構(gòu)是鎖、互斥鎖和旋轉(zhuǎn)鎖。

    非排他性鎖定

    非獨(dú)占鎖定允許您并發(fā)性。非獨(dú)占鎖定結(jié)構(gòu)是信號(hào)量(Slim)和ReaderWriterLock(Slim)。

    信號(hào)

    這些允許線程阻塞,直到收到來自其他線程的一個(gè)或多個(gè)通知。信令結(jié)構(gòu)包括手動(dòng)重置事件(Slim)、自動(dòng)重置事件、倒計(jì)時(shí)事件和屏障。前三個(gè)稱為。

    也可以(并且很棘手)通過使用在不鎖定的情況下對(duì)共享狀態(tài)執(zhí)行某些并發(fā)操作。這些是 Thread.MemoryBarrier 、Thread.volatileRead 、Thread.VolatileWrite、volatile 關(guān)鍵字和 Interlock 類。我們,以及監(jiān)視器的等待/脈沖方法,您可以使用它們來編寫自定義信令邏輯。

    獨(dú)家鎖定

    有三種獨(dú)占鎖定結(jié)構(gòu):lock 語句、Mutex 和 SpinLock。鎖結(jié)構(gòu)是最方便和廣泛使用的,而其他兩個(gè)目標(biāo)利基場(chǎng)景:

    • 互斥允許您跨越多個(gè)進(jìn)程(計(jì)算機(jī)范圍的鎖)。
    • SpinLock 實(shí)現(xiàn)了微優(yōu)化,可以減少高并發(fā)方案中的上下文切換(請(qǐng)參閱 )。

    鎖定聲明

    為了說明鎖定的必要性,請(qǐng)考慮以下類:

    class ThreadUnsafe
    {
      static int _val1 = 1, _val2 = 1;
    
      static void Go()
      {
        if (_val2 != 0) Console.WriteLine (_val1 / _val2);
        _val2 = 0;
      }
    }

    這個(gè)類不是線程安全的:如果兩個(gè)線程同時(shí)調(diào)用 Go,則可能會(huì)得到被零除錯(cuò)誤,因?yàn)開val2可以在一個(gè)線程中設(shè)置為零,因?yàn)榱硪粋€(gè)線程介于執(zhí)行 if 語句和 Console.WriteLine 之間。以下是鎖定解決問題的方法:

    class ThreadSafe
    {
      static readonly object _locker = new object();
      static int _val1 = 1, _val2 = 1;
    
      static void Go()
      {
        lock (_locker)
        {
          if (_val2 != 0) Console.WriteLine (_val1 / _val2);
          _val2 = 0;
        }
      }
    }

    一次只有一個(gè)線程可以鎖定同步對(duì)象(在本例中為 _locker),并且在釋放鎖之前,任何爭(zhēng)用線程都將被阻止。如果多個(gè)線程爭(zhēng)用鎖,它們將在“就緒隊(duì)列”上排隊(duì),并以先到先得的方式授予鎖。1有時(shí)說獨(dú)占鎖強(qiáng)制對(duì)受鎖保護(hù)的任何內(nèi)容進(jìn)行訪問,因?yàn)橐粋€(gè)線程的訪問不能與另一個(gè)線程的訪問重疊。在本例中,我們將保護(hù) Go 方法內(nèi)部的邏輯以及字段_val1和_val2。

    監(jiān)視。進(jìn)入和監(jiān)視。退出

    C# 的 lock 語句實(shí)際上是調(diào)用方法 Monitor.Enter 和 Monitor.Exit 的語法快捷方式,帶有 try / finally 塊。下面是前面示例的 Go 方法中實(shí)際發(fā)生的情況(簡(jiǎn)化版本):

    Monitor.Enter (_locker);
    try
    {
      if (_val2 != 0) Console.WriteLine (_val1 / _val2);
      _val2 = 0;
    }
    finally { Monitor.Exit (_locker); }

    在同一對(duì)象上調(diào)用 Monitor。退出而不先調(diào)用 Monitor.Enter 會(huì)引發(fā)異常。

    鎖已取重載

    我們剛剛演示的代碼有一個(gè)微妙的漏洞。考慮在調(diào)用 Monitor.Enter 和 try 塊之間引發(fā)異常的(不太可能的)事件(可能是由于 OutOfMemoryException,或者在 .NET Framework 中,如果線程中止)。在這種情況下,可能會(huì)也可能不會(huì)使用鎖定。如果鎖定占用,它不會(huì)被釋放 - 因?yàn)槲覀冇肋h(yuǎn)不會(huì)進(jìn)入 阻止。這將導(dǎo)致鎖泄漏。為了避免這種危險(xiǎn), 定義了以下重載:

    public static void Enter (object obj, ref bool lockTaken);

    lockTaken 在此方法之后為 false,如果(且僅當(dāng))Enter 方法引發(fā)異常并且未采用鎖定。

    下面是更可靠的使用模式(這正是 C# 翻譯鎖定語句的方式):

    bool lockTaken = false;
    try
    {
      Monitor.Enter (_locker, ref lockTaken);
      // Do your stuff...
    }
    finally { if (lockTaken) Monitor.Exit (_locker); }

    嘗試輸入

    監(jiān)視器還提供了 TryEnter 方法,該方法允許以毫秒為單位或以 TimeSpan 為單位指定超時(shí)。然后,如果獲取了鎖,則該方法返回 true;如果由于方法超時(shí)而未獲得鎖,則返回 false。也可以在沒有參數(shù)的情況下調(diào)用 TryEnter,這會(huì)“測(cè)試”鎖,如果無法立即獲取鎖,則會(huì)立即超時(shí)。與 Enter 方法一樣,TryEnter 被重載以接受 lockTaken 參數(shù)。

    選擇同步對(duì)象

    您可以將每個(gè)參與線程可見的任何對(duì)象用作同步對(duì)象,但要遵守一條硬性規(guī)則:它必須是引用類型。同步對(duì)象通常是私有的(因?yàn)檫@有助于封裝鎖定邏輯),并且通常是實(shí)例或靜態(tài)字段。同步對(duì)象可以兼作它所保護(hù)的對(duì)象,如以下示例中的_list字段所示:

    class ThreadSafe
    {
      List <string> _list = new List <string>();
    
      void Test()
      {
        lock (_list)
        {
          _list.Add ("Item 1");
          ...

    專用于鎖定的字段(例如前面示例中的 _locker)允許精確控制鎖定的范圍和粒度。您也可以使用包含對(duì)象 ( this ) 作為同步對(duì)象:

    lock (this) { ... }

    甚至它的類型:

    lock (typeof (Widget)) { ... }    // For protecting access to statics

    以這種方式鎖定的缺點(diǎn)是你沒有封裝鎖定邏輯,因此防止死鎖和過度阻塞變得更加困難。

    您還可以鎖定由 lambda 表達(dá)式或匿名方法捕獲的局部變量。

    注意

    鎖定不會(huì)以任何方式限制對(duì)同步對(duì)象本身的訪問。換句話說,x.ToString() 不會(huì)阻塞,因?yàn)榱硪粋€(gè)線程調(diào)用了 lock(x) ;兩個(gè)線程都必須調(diào)用 lock(x) 才能發(fā)生阻塞。

    何時(shí)鎖定

    作為基本規(guī)則,您需要鎖定訪問。即使在最簡(jiǎn)單的情況下(對(duì)單個(gè)字段執(zhí)行賦值操作),也必須考慮同步。在下面的類中,增量和賦值方法都不是線程安全的:

    class ThreadUnsafe
    {
      static int _x;
      static void Increment() { _x++; }
      static void Assign()    { _x = 123; }
    }

    以下是增量和分配的線程安全版本:

    static readonly object _locker = new object();
    static int _x;
    
    static void Increment() { lock (_locker) _x++; }
    static void Assign()    { lock (_locker) _x = 123; }

    如果沒有鎖,可能會(huì)出現(xiàn)兩個(gè)問題:

    • 諸如遞增變量(甚至在某些情況下讀取/寫入變量)之類的操作不是原子操作。
    • 編譯器、CLR 和處理器有權(quán)對(duì) CPU 寄存器中的指令和進(jìn)行重新排序以提高性能,只要此類優(yōu)化不會(huì)更改線程程序(或使用鎖的多線程程序)的行為。

    鎖定可以緩解第二個(gè)問題,因?yàn)樗鼤?huì)在鎖定之前和之后創(chuàng)建。內(nèi)存屏障是一個(gè)“圍欄”,重新排序和緩存的效果無法穿透它。

    注意

    這不僅適用于鎖,而且適用于所有同步構(gòu)造。因此,例如,如果您使用構(gòu)造,確保一次只有一個(gè)線程讀取/寫入變量,則無需鎖定。因此,以下代碼是線程安全的,無需鎖定 x :

    var signal = new ManualResetEvent (false);
    int x = 0;
    new Thread (() => { x++; signal.Set(); }).Start();
    signal.WaitOne();
    Console.WriteLine (x);    // 1 (always)

    在中,我們解釋了這種需求是如何產(chǎn)生的,以及內(nèi)存屏障和互鎖類如何在這些情況下提供鎖定的替代方案。

    鎖定和原子性

    如果一組變量總是在同一鎖中讀取和寫入,則可以說變量是以和寫入的。假設(shè)字段 x 和 y 總是在對(duì)象鎖上的鎖中讀取和分配:

    lock (locker) { if (x != 0) y /= x; }

    我們可以說 x 和 y 是原子訪問的,因?yàn)榇a塊不能被另一個(gè)線程的操作分割或搶占,以至于它會(huì)改變 x 或 y 并。您永遠(yuǎn)不會(huì)收到除以零的錯(cuò)誤,前提是 x 和 y 始終在同一獨(dú)占鎖中訪問。

    注意

    如果在鎖塊中引發(fā)異常(無論是否涉及多線程),則會(huì)違反鎖提供的原子性。例如,請(qǐng)考慮以下事項(xiàng):

    decimal _savingsBalance, _checkBalance;
    
    void Transfer (decimal amount)
    {
      lock (_locker)
      {
        _savingsBalance += amount;
        _checkBalance -= amount + GetBankFee();
      }
    }

    如果 GetBankFee() 拋出異常,銀行將虧損。在這種情況下,我們可以通過更早地調(diào)用GetBankFee來避免這個(gè)問題。對(duì)于更復(fù)雜的情況,解決方案是在捕獲或最終塊中實(shí)現(xiàn)“回滾”邏輯。

    指令原子性是一個(gè)不同的概念,盡管類似:如果指令在底層處理器上不可分割地執(zhí)行,則該是原子的。

    嵌套鎖定

    線程可以以嵌套()方式重復(fù)鎖定同一對(duì)象:

    lock (locker)
      lock (locker)
        lock (locker)
        {
           // Do something...
        }

    或者:

    Monitor.Enter (locker); Monitor.Enter (locker);  Monitor.Enter (locker); 
    // Do something...
    Monitor.Exit (locker);  Monitor.Exit (locker);   Monitor.Exit (locker);

    在這些情況下,僅當(dāng)最外層的鎖定語句退出或執(zhí)行了匹配數(shù)量的 Monitor.Exit 語句時(shí),才會(huì)解鎖對(duì)象。

    當(dāng)一種方法從鎖內(nèi)調(diào)用另一個(gè)方法時(shí),嵌套鎖定很有用:

    object locker = new object();
    
    lock (locker)
    {
      AnotherMethod();
      // We still have the lock - because locks are reentrant.
    }
    
    void AnotherMethod()
    {
      lock (locker) { Console.WriteLine ("Another method"); }
    }

    線程只能在第一個(gè)(最外層)鎖上阻塞。

    僵局

    當(dāng)兩個(gè)線程各自等待另一個(gè)線程持有的資源時(shí),就會(huì)發(fā)生死鎖,因此兩者都無法繼續(xù)。說明這一點(diǎn)的最簡(jiǎn)單方法是使用兩個(gè)鎖:

    object locker1 = new object();
    object locker2 = new object();
    
    new Thread (() => {
                        lock (locker1)
                        {
                          Thread.Sleep (1000);
                          lock (locker2);      // Deadlock
                        }
                      }).Start();
    lock (locker2)
    {
      Thread.Sleep (1000);
      lock (locker1);                          // Deadlock
    }

    您可以使用三個(gè)或更多線程創(chuàng)建更復(fù)雜的死鎖鏈。

    注意

    在標(biāo)準(zhǔn)宿主環(huán)境中,CLR 與 SQL Server 不同,不會(huì)通過終止其中一個(gè)違規(guī)者來自動(dòng)檢測(cè)和解決死鎖。線程死鎖會(huì)導(dǎo)致參與線程無限期阻塞,除非您指定了鎖定超時(shí)。(但是,在 SQL CLR 集成主機(jī)下,檢測(cè)到死鎖,并在其中一個(gè)線程上引發(fā) [可捕獲] 異常。

    死鎖是多線程處理中最困難的問題之一,尤其是當(dāng)存在許多相互關(guān)聯(lián)的對(duì)象時(shí)。從根本上說,困難的問題是您無法確定取出了哪些鎖。

    因此,您可能會(huì)在類 x 中鎖定私人字段 a,而不知道您的調(diào)用方(或調(diào)用方的調(diào)用方)已經(jīng)鎖定了類 y 中的字段 b。與此同時(shí),另一個(gè)線程正在做相反的事情 - 創(chuàng)建死鎖。具有諷刺意味的是,(好的)面向?qū)ο笤O(shè)計(jì)模式加劇了這個(gè)問題,因?yàn)榇祟惸J絼?chuàng)建的調(diào)用鏈直到運(yùn)行時(shí)才確定。

    流行的建議“以一致的順序鎖定對(duì)象以防止死鎖”雖然在我們的初始示例中很有幫助,但很難應(yīng)用于剛才描述的方案。更好的策略是警惕鎖定對(duì)象中對(duì)方法的調(diào)用,這些方法可能引用回您自己的對(duì)象。此外,考慮是否真的需要鎖定對(duì)其他類中方法的調(diào)用(通常你會(huì)這樣做——正如中看到的那樣——但有時(shí)還有其他選項(xiàng))。更多地依賴更高級(jí)別的同步選項(xiàng),如任務(wù)延續(xù)/組合器、數(shù)據(jù)并行性和不可變類型(本章后面)可以減少對(duì)鎖定的需求。

    注意

    這是感知問題的另一種方法:當(dāng)您在握住鎖時(shí)調(diào)用其他代碼時(shí),該鎖的封裝會(huì)微妙地。這不是 CLR 中的錯(cuò)誤;一般來說,這是鎖定的基本限制。鎖定問題正在各種研究項(xiàng)目中得到解決,包括。

    在擁有鎖的情況下調(diào)用 Dispatcher.Invoke(在 WPF 應(yīng)用程序中)或 Control.Invoke(在 Windows 窗體應(yīng)用程序中)時(shí),會(huì)出現(xiàn)另一種死鎖情況。如果用戶界面碰巧正在運(yùn)行另一個(gè)正在等待同一鎖的方法,則死鎖將在那里發(fā)生。通常,只需調(diào)用 BeginInvoke 而不是 Invoke(或依賴于在存在同步上下文時(shí)隱式執(zhí)行此操作的異步函數(shù))即可解決此問題。或者,您可以在調(diào)用 Invoke 之前釋放鎖,盡管如果您的取出鎖,這將不起作用。

    性能

    鎖定速度很快:如果鎖沒有爭(zhēng)用,您可以在 20 年代的計(jì)算機(jī)上在不到 2020 納秒的時(shí)間內(nèi)獲取和釋放鎖。如果發(fā)生爭(zhēng)用,則相應(yīng)的上下文切換會(huì)將開銷移近微秒?yún)^(qū)域,盡管在實(shí)際重新調(diào)度線程之前可能會(huì)更長(zhǎng)。

    互斥體

    互斥鎖類似于C#鎖,但它可以跨多個(gè)進(jìn)程工作。換句話說,互斥可以是范圍的,也可以是。獲取和釋放無爭(zhēng)議的互斥體大約需要半微秒,比鎖慢 20 多倍。

    使用互斥類,可以調(diào)用 WaitOne 方法進(jìn)行鎖定,并調(diào)用 ReleaseMutex 進(jìn)行解鎖。與 lock 語句一樣,互斥體只能從獲取它的同一線程中釋放。

    注意

    如果你忘記調(diào)用 ReleaseMutex 而只是調(diào)用 Close 或 Dispose,一個(gè)廢棄的互斥例外將被拋給等待該互斥鎖的其他人。

    跨進(jìn)程互斥體的常見用途是確保一次只能運(yùn)行一個(gè)程序?qū)嵗_@是它是如何完成的:

    // Naming a Mutex makes it available computer-wide. Use a name that's
    // unique to your company and application (e.g., include your URL).
    
    using var mutex = new Mutex (true, @"Global\oreilly.com OneAtATimeDemo");
    // Wait a few seconds if contended, in case another instance
    // of the program is still in the process of shutting down.
    
    if (!mutex.WaitOne (TimeSpan.FromSeconds (3), false))
    {
      Console.WriteLine ("Another instance of the app is running. Bye!");
      return;
    }
    try { RunProgram(); }
    finally { mutex.ReleaseMutex (); }
    
    void RunProgram()
    {
      Console.WriteLine ("Running. Press Enter to exit");
      Console.ReadLine();
    }

    注意

    如果在終端服務(wù)下或在單獨(dú)的 Unix 控制臺(tái)中運(yùn)行,則計(jì)算機(jī)范圍的互斥體通常僅對(duì)同一會(huì)話中的應(yīng)用程序可見。若要使其對(duì)所有終端服務(wù)器會(huì)話可見,請(qǐng)?jiān)谄涿Q前面加上 ,如示例中所示。

    鎖定和螺紋安全

    如果程序或方法可以在任何多線程方案中正常工作,則它是線程安全的。線程安全主要通過鎖定和減少線程交互的可能性來實(shí)現(xiàn)。

    常規(guī)用途類型很少完全是線程安全的,

    • 全線程安全中的開發(fā)負(fù)擔(dān)可能很大,特別是如果類型具有許多字段(每個(gè)字段在任意多線程上下文中都有交互的可能性)。
    • 線程安全可能會(huì)帶來性能成本(無論該類型是否實(shí)際由多個(gè)線程使用,都應(yīng)支付部分費(fèi)用)。
    • 線程安全類型不一定使使用它的程序成為線程安全的,后者所涉及的工作通常使前者變得多余。

    因此,線程安全通常只在需要的地方實(shí)現(xiàn),以便處理特定的多線程方案。

    但是,有幾種方法可以“作弊”,并使大型復(fù)雜類在多線程環(huán)境中安全運(yùn)行。一種是通過將大部分代碼(甚至訪問整個(gè)對(duì)象)包裝在單個(gè)獨(dú)占鎖中來犧牲粒度,從而在高級(jí)別強(qiáng)制執(zhí)行序列化訪問。事實(shí)上,如果要在多線程上下文中使用線程不安全的第三方代碼(或大多數(shù) .NET 類型),則此策略是必不可少的。訣竅只是使用相同的獨(dú)占鎖來保護(hù)對(duì)線程不安全對(duì)象上所有屬性、方法和字段的訪問。如果對(duì)象的方法都快速執(zhí)行,則解決方案效果很好(否則,會(huì)有很多阻塞)。

    注意

    撇開基元類型不談,很少有 .NET 類型在實(shí)例化時(shí),除了并發(fā)只讀訪問之外,對(duì)線程安全。開發(fā)人員有責(zé)任疊加線程安全性,通常使用獨(dú)占鎖。(我們?cè)谥薪榻B的 System.Collections.Concurrent 中的集合是一個(gè)例外。

    作弊的另一種方法是通過最小化共享數(shù)據(jù)來最小化線程交互。這是一個(gè)很好的方法,隱式用于“無狀態(tài)”中間層應(yīng)用程序和網(wǎng)頁服務(wù)器。由于多個(gè)客戶端請(qǐng)求可以同時(shí)到達(dá),因此它們調(diào)用的服務(wù)器方法必須是線程安全的。無狀態(tài)設(shè)計(jì)(由于可伸縮性的原因而流行)本質(zhì)上限制了交互的可能性,因?yàn)轭惒粫?huì)在請(qǐng)求之間保存數(shù)據(jù)。然后,線程交互僅限于您可能選擇創(chuàng)建的靜態(tài)字段,用于在內(nèi)存中緩存常用數(shù)據(jù)以及提供基礎(chǔ)結(jié)構(gòu)服務(wù)(如身份驗(yàn)證和審核)等目的。

    另一種解決方案(在胖客戶端應(yīng)用程序中)是在 UI 線程上運(yùn)行訪問共享狀態(tài)的代碼。正如我們?cè)诘?中看到的,異步函數(shù)使這變得容易。

    線程安全和 .NET 類型

    可以使用鎖定將線程不安全代碼轉(zhuǎn)換為線程安全代碼。.NET 是一個(gè)很好的應(yīng)用:幾乎所有的非基元類型在實(shí)例化時(shí)都不是線程安全的(除了只讀訪問之外的任何內(nèi)容),但如果對(duì)任何給定對(duì)象的所有訪問都通過鎖進(jìn)行保護(hù),則可以在多線程代碼中使用它們。下面是兩個(gè)線程同時(shí)將項(xiàng)添加到同一 List 集合,然后枚舉該列表的示例:

    class ThreadSafe
    {
      static List <string> _list = new List <string>();
    
      static void Main()
      {
        new Thread (AddItem).Start();
        new Thread (AddItem).Start();
      }
    
      static void AddItem()
      {
        lock (_list) _list.Add ("Item " + _list.Count);
    
        string[] items;
        lock (_list) items = _list.ToArray();
        foreach (string s in items) Console.WriteLine (s);
      }
    }

    在本例中,我們將鎖定_list對(duì)象本身。如果我們有兩個(gè)相互關(guān)聯(lián)的列表,我們需要選擇一個(gè)要鎖定的公共對(duì)象(我們可以指定其中一個(gè)列表,或者更好的是:使用一個(gè)獨(dú)立的字段)。

    枚舉 .NET 集合也是線程不安全的,因?yàn)槿绻诿杜e期間修改列表,則會(huì)引發(fā)異常。在此示例中,我們首先將項(xiàng)目復(fù)制到數(shù)組中,而不是在枚舉期間鎖定。這樣可以避免在枚舉期間執(zhí)行的操作可能非常耗時(shí)時(shí)過度持有鎖。(另一種解決方案是使用讀取器/寫入器鎖;請(qǐng)參閱

    鎖定線程安全對(duì)象

    有時(shí),您還需要鎖定訪問線程安全對(duì)象。為了說明這一點(diǎn),想象一下.NET 的 List 類確實(shí)是線程安全的,我們希望將一個(gè)項(xiàng)目添加到列表中:

    if (!_list.Contains (newItem)) _list.Add (newItem);

    無論列表是否線程安全,此聲明肯定不是!整個(gè) if 語句需要包裝在鎖中,以防止在測(cè)試集裝箱船和添加新項(xiàng)目之間搶占。然后,在我們修改該列表的任何地方都需要使用相同的鎖。例如,以下語句也需要包裝在相同的鎖中,以確保它不會(huì)搶占前一個(gè)語句:

    _list.Clear();

    換句話說,我們需要完全像線程不安全的集合類一樣鎖定(使 List 類的假設(shè)線程安全冗余)。

    注意

    在高并發(fā)環(huán)境中,鎖定訪問集合可能會(huì)導(dǎo)致過度阻塞。為此,.NET 提供了一個(gè)線程安全的隊(duì)列、堆棧和字典,我們將在第 中討論。

    靜態(tài)成員

    僅當(dāng)所有并發(fā)線程都知道并使用鎖時(shí),才可以圍繞自定義鎖包裝對(duì)對(duì)象的訪問。如果對(duì)象的作用域較廣,則情況可能并非如此。最壞的情況是公共類型中的靜態(tài)成員。例如,假設(shè) DateTime 結(jié)構(gòu) DateTime.Now 上的靜態(tài)屬性不是線程安全的,并且兩個(gè)并發(fā)調(diào)用可能會(huì)導(dǎo)致亂碼輸出或異常。使用外部鎖定解決此問題的唯一方法可能是在調(diào)用 DateTime.Now 之前鎖定類型本身 — lock(typeof(DateTime)) 。這只有在所有程序員都同意這樣做的情況下才會(huì)起作用(這不太可能)。此外,鎖定類型會(huì)產(chǎn)生其自身的問題。

    因此,DateTime 結(jié)構(gòu)上的靜態(tài)成員已經(jīng)過精心編程,使其是線程安全的。這是整個(gè) .NET 中的常見模式:靜態(tài)成員在編寫供公眾使用的類型時(shí),遵循此模式也是有意義的,以免創(chuàng)建不可能的線程安全難題。換句話說,通過使靜態(tài)方法成為線程安全的,您正在編程,以免該類型使用者的線程安全。

    注意

    靜態(tài)方法中的線程安全是您必須顯式編碼的東西:由于方法是靜態(tài)的,它不會(huì)自動(dòng)發(fā)生!

    只讀線程安全

    使類型對(duì)并發(fā)只讀訪問(如果可能)是線程安全的,因?yàn)檫@意味著使用者可以避免過度鎖定。許多 .NET 類型遵循此原則:例如,集合對(duì)于并發(fā)讀取器是線程安全的。

    遵循此原則很簡(jiǎn)單:如果將某個(gè)類型記錄為線程安全的并發(fā)只讀訪問,則不要寫入使用者期望為只讀的方法中的字段(或鎖定這樣做)。例如,在集合中實(shí)現(xiàn) ToArray() 方法時(shí),可以從壓縮集合的內(nèi)部結(jié)構(gòu)開始。但是,對(duì)于希望這是只讀的使用者來說,這將使線程不安全。

    只讀線程安全是枚舉器與“可枚舉項(xiàng)”分開的原因之一:兩個(gè)線程可以同時(shí)枚舉一個(gè)集合,因?yàn)槊總€(gè)線程都獲取一個(gè)單獨(dú)的枚舉器對(duì)象。

    注意

    在沒有文檔的情況下,在假設(shè)方法本質(zhì)上是否是只讀時(shí)要謹(jǐn)慎。一個(gè)很好的例子是 Random 類:當(dāng)你調(diào)用 Random.Next() 時(shí),它的內(nèi)部實(shí)現(xiàn)要求它更新私有種子值。因此,必須使用 Random 類進(jìn)行鎖定,或者為每個(gè)線程維護(hù)一個(gè)單獨(dú)的實(shí)例。

    應(yīng)用程序服務(wù)器中的線程安全

    應(yīng)用程序服務(wù)器需要多線程來處理并發(fā)的客戶端請(qǐng)求。ASP.NET 核心和 Web API 應(yīng)用程序都是隱式多線程的。這意味著,在服務(wù)器端編寫代碼時(shí),如果處理客戶端請(qǐng)求的線程之間可能存在交互,則必須考慮線程安全性。幸運(yùn)的是,這種可能性很少見;典型的服務(wù)器類要么是無狀態(tài)的(無字段),要么具有為每個(gè)客戶端或每個(gè)請(qǐng)求創(chuàng)建單獨(dú)對(duì)象實(shí)例的激活模型。交互通常僅通過靜態(tài)字段產(chǎn)生,有時(shí)用于在數(shù)據(jù)庫的內(nèi)存部分中緩存以提高性能。

    例如,假設(shè)您有一個(gè)查詢數(shù)據(jù)庫的 RetrieveUser 方法:

    // User is a custom class with fields for user data
    internal User RetrieveUser (int id) { ... }

    如果經(jīng)常調(diào)用此方法,則可以通過在靜態(tài)字典中緩存結(jié)果來提高性能。下面是一個(gè)概念上簡(jiǎn)單的解決方案,它考慮了線程安全性:

    static class UserCache
    {
      static Dictionary <int, User> _users = new Dictionary <int, User>();
    
      internal static User GetUser (int id)
      {
        User u = null;
    
        lock (_users)
          if (_users.TryGetValue (id, out u))
            return u;
    
        u = RetrieveUser (id);           // Method to retrieve from database;
        lock (_users) _users [id] = u;
        return u;
      }
    }

    我們至少必須鎖定閱讀和更新字典以確保線程安全。在此示例中,我們?cè)阪i定的簡(jiǎn)單性和性能之間選擇了實(shí)際折衷方案。我們的設(shè)計(jì)造成了效率低下的小可能性:如果兩個(gè)線程同時(shí)使用相同的以前未檢索的 id 調(diào)用此方法,則 RetrieveUser 方法將被調(diào)用兩次,并且字典將不必要地更新。在整個(gè)方法中鎖定一次可以防止這種情況,但它會(huì)造成更糟糕的低效率:整個(gè)緩存將在調(diào)用 RetrieveUser 期間被鎖定,在此期間,其他線程將在檢索用戶時(shí)被阻止。

    對(duì)于理想的解決方案,我們需要使用我們?cè)谥忻枋龅牟呗浴N覀儾皇蔷彺?User ,而是緩存 Task<User> ,然后調(diào)用者等待:

    static class UserCache
    {
      static Dictionary <int, Task<User>> _userTasks = 
         new Dictionary <int, Task<User>>();
      
      internal static Task<User> GetUserAsync (int id)
      {
        lock (_userTasks)
          if (_userTasks.TryGetValue (id, out var userTask))
            return userTask;
          else
            return _userTasks [id] = Task.Run (() => RetrieveUser (id));
      }
    }

    請(qǐng)注意,我們現(xiàn)在有一個(gè)涵蓋整個(gè)方法邏輯的鎖。我們可以在不損害并發(fā)性的情況下做到這一點(diǎn),因?yàn)槲覀冊(cè)阪i內(nèi)所做的只是訪問字典并(可能)異步操作(通過調(diào)用 Task.Run)。如果兩個(gè)線程使用相同的 ID 同時(shí)調(diào)用此方法,它們最終都會(huì)等待,這正是我們想要的結(jié)果。

    不可變對(duì)象

    不可變對(duì)象是其狀態(tài)無法更改的對(duì)象 - 外部或內(nèi)部。不可變對(duì)象中的字段通常聲明為只讀,并在構(gòu)造期間完全初始化。

    不變性是函數(shù)式編程的標(biāo)志,在函數(shù)式編程中,您不是對(duì)象,而是創(chuàng)建具有不同屬性的新對(duì)象。LINQ 遵循此范例。不變性在多線程中也很有價(jià)值,因?yàn)樗ㄟ^消除(或最小化)可寫狀態(tài)來避免共享可寫狀態(tài)的問題。

    一種模式是使用不可變對(duì)象來封裝一組相關(guān)字段,以最大程度地減少鎖定持續(xù)時(shí)間。舉一個(gè)非常簡(jiǎn)單的例子,假設(shè)我們有兩個(gè)字段,如下所示:

    int _percentComplete;
    string _statusMessage;

    現(xiàn)在讓我們假設(shè)我們想以原子方式讀取和寫入它們。與其鎖定這些字段,我們可以定義以下不可變類:

    class ProgressStatus    // Represents progress of some activity
    {
      public readonly int PercentComplete;
      public readonly string StatusMessage;
    
      // This class might have many more fields...
    
      public ProgressStatus (int percentComplete, string statusMessage)
      {
        PercentComplete = percentComplete;
        StatusMessage = statusMessage;
      }
    }

    然后我們可以定義該類型的單個(gè)字段,以及一個(gè)鎖定對(duì)象:

    readonly object _statusLocker = new object();
    ProgressStatus _status;

    現(xiàn)在,我們可以讀取和寫入該類型的值,而無需為多個(gè)賦值保留鎖:

    var status = new ProgressStatus (50, "Working on it");
    // Imagine we were assigning many more fields...
    // ...
    lock (_statusLocker) _status = status;    // Very brief lock

    要讀取對(duì)象,我們首先獲取對(duì)象引用的副本(在鎖內(nèi))。然后,我們可以讀取其值而無需按住鎖:

    ProgressStatus status;
    lock (_statusLocker) status = _status;   // Again, a brief lock
    int pc = status.PercentComplete;
    string msg = status.StatusMessage;
    ...

    非排他性鎖定

    非獨(dú)占鎖定構(gòu)造用于并發(fā)性。在本節(jié)中,我們將介紹信號(hào)量和讀/寫器鎖,并說明 SemaphoreSlim 類如何限制異步操作的并發(fā)性。

    信號(hào)

    信號(hào)燈就像一個(gè)夜總會(huì):它有一定的容量,由保鏢強(qiáng)制執(zhí)行。當(dāng)俱樂部滿員時(shí),沒有更多的人可以進(jìn)入,外面排起了長(zhǎng)隊(duì)。然后,對(duì)于每個(gè)離開的人,有一個(gè)人進(jìn)入。構(gòu)造函數(shù)至少需要兩個(gè)參數(shù):夜總會(huì)當(dāng)前可用的位置數(shù)和俱樂部的總?cè)萘俊?/p>

    容量為 1 的信號(hào)量類似于互斥鎖或鎖,只是信號(hào)量沒有“所有者”——它與。任何線程都可以在信號(hào)量上調(diào)用釋放,而對(duì)于互斥鎖和鎖,只有獲得鎖的線程才能釋放它。

    注意

    此類有兩個(gè)功能相似的版本:信號(hào)量和信號(hào)量Slim。后者已經(jīng)過優(yōu)化,可滿足并行編程的低延遲需求。它在傳統(tǒng)的多線程中也很有用,因?yàn)樗试S您在等待時(shí)指定取消令牌(請(qǐng)參閱中的),并且它公開了用于異步編程的 WaitAsync 方法。但是,不能將其用于進(jìn)程間信號(hào)。

    信號(hào)量在調(diào)用 WaitOne 和 Release 時(shí)會(huì)產(chǎn)生大約一微秒;信號(hào)量Slim約占其中的十分之一。

    信號(hào)量可用于限制并發(fā)性,防止太多線程同時(shí)執(zhí)行特定代碼段。在以下示例中,五個(gè)線程嘗試進(jìn)入一次只允許三個(gè)線程進(jìn)入的夜總會(huì):

    class TheClub      // No door lists!
    {
      static SemaphoreSlim _sem = new SemaphoreSlim (3);    // Capacity of 3
     
      static void Main()
      {
        for (int i = 1; i <= 5; i++) new Thread (Enter).Start (i);
      }
    
      static void Enter (object id)
      {
        Console.WriteLine (id + " wants to enter");
        _sem.Wait();
        Console.WriteLine (id + " is in!");           // Only three threads
        Thread.Sleep (1000 * (int) id);               // can be here at
        Console.WriteLine (id + " is leaving");       // a time.
        _sem.Release();
      }
    }
    
    1 wants to enter
    1 is in!
    2 wants to enter
    2 is in!
    3 wants to enter
    3 is in!
    4 wants to enter
    5 wants to enter
    1 is leaving
    4 is in!
    2 is leaving
    5 is in!

    信號(hào)量,如果命名,可以像互斥體一樣跨越進(jìn)程(名為信號(hào)量?jī)H在Windows上可用,而命名的互斥體也適用于Unix平臺(tái))。

    異步信號(hào)量和鎖

    鎖定 await 語句是非法的:

    lock (_locker)
    {
      await Task.Delay (1000);    // Compilation error
      ...
    }

    這樣做是沒有意義的,因?yàn)殒i由線程持有,線程通常在從等待返回時(shí)發(fā)生變化。鎖定也會(huì),而阻塞可能很長(zhǎng)的時(shí)間正是你用異步函數(shù)實(shí)現(xiàn)的。

    但是,有時(shí)仍然需要使異步操作按順序執(zhí)行,或者限制并行性,以便一次執(zhí)行的操作不超過 個(gè)。例如,考慮一個(gè) Web 瀏覽器:它需要并行執(zhí)行異步下載,但它可能希望施加一個(gè)限制,以便一次最多進(jìn)行 10 次下載。我們可以通過使用信號(hào)量苗來實(shí)現(xiàn)這一點(diǎn):

    SemaphoreSlim _semaphore = new SemaphoreSlim (10);
    
    async Task<byte[]> DownloadWithSemaphoreAsync (string uri)
    {
        await _semaphore.WaitAsync();
        try { return await new WebClient().DownloadDataTaskAsync (uri); }
        finally { _semaphore.Release(); }
    }

    將信號(hào)燈的 initialCount 減少到 1 會(huì)將最大并行度降低到 1,從而將其轉(zhuǎn)換為異步鎖。

    編寫輸入異步擴(kuò)展方法

    以下擴(kuò)展方法通過使用我們?cè)谥芯帉懙囊淮涡灶惡?jiǎn)化了 SemaphoreSlim 的異步使用:

    public static async Task<IDisposable> EnterAsync (this SemaphoreSlim ss)
    {
      await ss.WaitAsync().ConfigureAwait (false);
      return Disposable.Create (() => ss.Release());
    }

    使用此方法,我們可以重寫我們的 DownloadWithSemaphoreAsync :

    async Task<byte[]> DownloadWithSemaphoreAsync (string uri)
    {
      using (await _semaphore.EnterAsync())
        return await new WebClient().DownloadDataTaskAsync (uri);
    }

    Parallel.ForEachAsync

    從 .NET 6 開始,限制異步并發(fā)的另一種方法是使用 Parallel.ForEachAsync 方法。假設(shè) uri 位于要下載的 URI 數(shù)組中,下面介紹了如何并行下載它們,同時(shí)將并發(fā)性限制為最多 10 次并行下載:

    await Parallel.ForEachAsync (uris,
      new ParallelOptions { MaxDegreeOfParallelism = 10 },
      async (uri, cancelToken) =>
       {
        var download = await new HttpClient().GetByteArrayAsync (uri);
        Console.WriteLine ($"Downloaded {download.Length} bytes");
      });

    并行類中的其他方法適用于(計(jì)算綁定的)并行編程方案,我們將在第 中介紹。

    讀/寫器鎖

    通常,某種類型的實(shí)例對(duì)于并發(fā)讀取操作是線程安全的,但對(duì)于并發(fā)更新(或?qū)τ诓l(fā)讀取和更新)則不是線程安全的。對(duì)于文件等資源也是如此。盡管使用適用于所有訪問模式的簡(jiǎn)單獨(dú)占鎖保護(hù)此類類型的實(shí)例通常可以解決問題,但如果有許多讀取器和偶爾更新,則可能會(huì)不合理地限制并發(fā)性。在業(yè)務(wù)應(yīng)用程序服務(wù)器中,可能會(huì)發(fā)生這種情況的一個(gè)示例,該服務(wù)器緩存常用數(shù)據(jù)以便在靜態(tài)字段中快速檢索。類旨在在此提供最大可用性鎖定。

    注意

    ReaderWriterLockSlim是舊的“胖”ReaderWriterLock類的替代品。后者在功能上相似,但它的速度要慢幾倍,并且在其處理鎖升級(jí)的機(jī)制中存在固有的設(shè)計(jì)缺陷。

    與普通鎖(監(jiān)視器.進(jìn)入/退出)相比,ReaderWriterLockSlim仍然慢兩倍。權(quán)衡是減少爭(zhēng)用(當(dāng)有大量閱讀和最少的寫作時(shí))。

    對(duì)于這兩個(gè)類,有兩種基本的鎖類型:讀鎖定和寫鎖定:

    • 寫鎖定是通用獨(dú)占的。
    • 讀鎖定與其他讀鎖定兼容。

    因此,持有寫鎖的線程會(huì)阻止所有其他嘗試獲取讀寫鎖的線程(反之亦然)。但是,如果沒有線程持有寫鎖定,則任意數(shù)量的線程可以同時(shí)獲得讀鎖定。

    ReaderWriterLockSlim定義了以下獲取和釋放讀/寫鎖的方法:

    public void EnterReadLock();
    public void ExitReadLock();
    public void EnterWriteLock();
    public void ExitWriteLock();

    此外,所有方法都有“Try”版本,它們接受 Monitor.TryEnter 樣式的超時(shí)參數(shù)(如果資源嚴(yán)重爭(zhēng)用,則很容易發(fā)生超時(shí))。ReaderWriterLock 提供了類似的方法,命名為 和 。如果發(fā)生超時(shí),它們會(huì)拋出應(yīng)用程序異常,而不是返回 false 。EnterXXXAcquireXXXReleaseXXX

    以下程序演示了 ReaderWriterLockSlim 。三個(gè)線程連續(xù)枚舉一個(gè)列表,而另外兩個(gè)線程每 100 毫秒向列表追加一個(gè)隨機(jī)數(shù)。讀鎖定保護(hù)列表讀取器,寫鎖定保護(hù)列表編寫器:

    class SlimDemo
    {
      static ReaderWriterLockSlim _rw = new ReaderWriterLockSlim();
      static List<int> _items = new List<int>();
      static Random _rand = new Random();
    
      static void Main()
      {
        new Thread (Read).Start();
        new Thread (Read).Start();
        new Thread (Read).Start();
    
        new Thread (Write).Start ("A");
        new Thread (Write).Start ("B");
      }
    
      static void Read()
      {
        while (true)
        {
          _rw.EnterReadLock();
          foreach (int i in _items) Thread.Sleep (10);
          _rw.ExitReadLock();
        }
      }
    
      static void Write (object threadID)
      {
        while (true)
        {
          int newNumber = GetRandNum (100);
          _rw.EnterWriteLock();
          _items.Add (newNumber);
          _rw.ExitWriteLock();
          Console.WriteLine ("Thread " + threadID + " added " + newNumber);
          Thread.Sleep (100);
        }
      }
    
      static int GetRandNum (int max) { lock (_rand) return _rand.Next(max); }
    }

    注意

    在生產(chǎn)代碼中,通常會(huì)添加 try / finally 塊,以確保在引發(fā)異常時(shí)釋放鎖。

    結(jié)果如下:

    Thread B added 61
    Thread A added 83
    Thread B added 55
    Thread A added 33
    ...

    ReaderWriterLockSlim 允許比簡(jiǎn)單鎖更多的并發(fā)讀取活動(dòng)。我們可以通過在 while 循環(huán)的開頭的 Write 方法中插入以下行來說明這一點(diǎn):

    Console.WriteLine (_rw.CurrentReadCount + " concurrent readers");

    這幾乎總是打印“3 個(gè)并發(fā)讀取器”(Read 方法將大部分時(shí)間花在 foreach 循環(huán)中)。除了 CurrentReadCount 之外,ReaderWriterLockSlim 還提供了以下屬性來監(jiān)視鎖:

    public bool IsReadLockHeld            { get; }
    public bool IsUpgradeableReadLockHeld { get; }
    public bool IsWriteLockHeld           { get; }
    
    public int  WaitingReadCount          { get; }
    public int  WaitingUpgradeCount       { get; }
    public int  WaitingWriteCount         { get; }
    
    public int  RecursiveReadCount        { get; }
    public int  RecursiveUpgradeCount     { get; }
    public int  RecursiveWriteCount       { get; }

    可升級(jí)鎖

    有時(shí),在單個(gè)原子操作中將讀鎖定交換為寫鎖定很有用。例如,假設(shè)您希望僅在某個(gè)項(xiàng)目尚不存在時(shí)才將該項(xiàng)目添加到列表中。理想情況下,您希望最大程度地減少持有(獨(dú)占)寫鎖定所花費(fèi)的時(shí)間,因此您可以執(zhí)行以下操作:

    1. 獲取讀鎖定。
    2. 測(cè)試該項(xiàng)目是否已存在于列表中;如果是這樣,請(qǐng)松開鎖并返回 。
    3. 釋放讀鎖定。
    4. 獲取寫鎖定。
    5. 添加項(xiàng)目。

    問題是另一個(gè)線程可能會(huì)潛入并在步驟 3 和 4 之間修改列表(例如,添加相同的項(xiàng)目)。ReaderWriterLockSlim通過第三種鎖(稱為)來解決這個(gè)問題。可升級(jí)鎖類似于讀鎖,只是它以后可以在原子操作中提升為寫鎖。以下是您的使用方法:

    1. 調(diào)用 EnterUpgradeableReadLock 。
    2. 執(zhí)行基于讀取的活動(dòng)(例如,測(cè)試項(xiàng)目是否已存在于列表中)。
    3. 調(diào)用 EnterWriteLock(這會(huì)將可升級(jí)鎖轉(zhuǎn)換為寫鎖)。
    4. 執(zhí)行基于寫入的活動(dòng)(例如,將項(xiàng)目添加到列表中)。
    5. 調(diào)用 ExitWriteLock(這會(huì)將寫鎖定轉(zhuǎn)換回可升級(jí)的鎖)。
    6. 執(zhí)行任何其他基于讀取的活動(dòng)。
    7. 調(diào)用退出可升級(jí)讀取鎖定。

    從調(diào)用者的角度來看,這更像是嵌套或遞歸鎖定。不過,從功能上講,在步驟 3 中,ReaderWriterLockSlim 會(huì)釋放您的讀鎖定,并以原子方式獲得新的寫鎖定。

    可升級(jí)鎖和讀鎖之間還有另一個(gè)重要區(qū)別。盡管可升級(jí)鎖可以與任意數(shù)量的鎖共存,但一次只能取出一個(gè)可升級(jí)鎖本身。這可以通過競(jìng)爭(zhēng)轉(zhuǎn)換來防止轉(zhuǎn)換死鎖,就像 SQL Server 中的更新鎖一樣:

    SQL Server

    讀卡器作家鎖苗條

    共享鎖定

    讀鎖定

    專屬鎖

    寫鎖定

    更新鎖

    可升級(jí)鎖

    我們可以通過更改前面示例中的 Write 方法來演示可升級(jí)鎖,以便僅在數(shù)字不存在時(shí)才向列表中添加一個(gè)數(shù)字:

    while (true)
    {
      int newNumber = GetRandNum (100);
      _rw.EnterUpgradeableReadLock();
      if (!_items.Contains (newNumber))
      {
        _rw.EnterWriteLock();
        _items.Add (newNumber);
        _rw.ExitWriteLock();
        Console.WriteLine ("Thread " + threadID + " added " + newNumber);
      }
      _rw.ExitUpgradeableReadLock();
      Thread.Sleep (100);
    }

    注意

    ReaderWriterLock 也可以進(jìn)行鎖轉(zhuǎn)換,但不可靠,因?yàn)樗恢С挚缮?jí)鎖的概念。這就是為什么ReaderWriterLockSlim的設(shè)計(jì)者必須從一個(gè)新的類重新開始。

    鎖遞歸

    通常,嵌套或遞歸鎖定是禁止使用ReaderWriterLockSlim的。因此,以下內(nèi)容會(huì)引發(fā)異常:

    var rw = new ReaderWriterLockSlim();
    rw.EnterReadLock();
    rw.EnterReadLock();
    rw.ExitReadLock();
    rw.ExitReadLock();

    但是,如果您按如下方式構(gòu)造 ReaderWriterLockSlim,它就會(huì)運(yùn)行而不會(huì)出錯(cuò):

    var rw = new ReaderWriterLockSlim (LockRecursionPolicy.SupportsRecursion);

    這可確保遞歸鎖定只有在您計(jì)劃的情況下才能發(fā)生。遞歸鎖定可能會(huì)產(chǎn)生不希望的復(fù)雜性,因?yàn)榭赡軙?huì)獲取多種鎖:

    rw.EnterWriteLock();
    rw.EnterReadLock();
    Console.WriteLine (rw.IsReadLockHeld);     // True
    Console.WriteLine (rw.IsWriteLockHeld);    // True
    rw.ExitReadLock();
    rw.ExitWriteLock();

    基本規(guī)則是,在獲得鎖后,后續(xù)遞歸鎖可以更少,但不能更大,在以下規(guī)模上:

    讀鎖→可升級(jí)鎖→寫鎖

    但是,將可升級(jí)鎖提升為寫鎖的請(qǐng)求始終是合法的。

    使用事件等待句柄發(fā)出信號(hào)

    最簡(jiǎn)單的信令構(gòu)造稱為(與 C# 事件無關(guān))。事件等待句柄有三種形式:自動(dòng)重置事件、手動(dòng)重置事件(苗條)和倒計(jì)時(shí)事件。前兩者基于公共 EventWaitHandle 類,它們從該類派生所有功能。

    自動(dòng)重置事件

    自動(dòng)重置事件就像一個(gè)票務(wù)旋轉(zhuǎn)門:插入一張票證只能讓一個(gè)人通過。類名稱中的“auto”是指打開的十字轉(zhuǎn)門在有人通過后自動(dòng)關(guān)閉或“重置”的事實(shí)。線程通過調(diào)用 WaitOne 在十字轉(zhuǎn)門處等待或阻塞(在此“一”十字轉(zhuǎn)門處等待,直到它打開),并通過調(diào)用 Set 方法插入票證。如果多個(gè)線程調(diào)用 WaitOne ,則隊(duì)列2在十字轉(zhuǎn)門后面堆積。票證可以來自任何線程;換句話說,任何有權(quán)訪問 AutoResetEvent 對(duì)象的(未阻止的)線程都可以在其上調(diào)用 Set 以釋放一個(gè)阻塞的線程。

    您可以通過兩種方式創(chuàng)建自動(dòng)重置事件。第一個(gè)是通過其構(gòu)造函數(shù):

    var auto = new AutoResetEvent (false);

    (將 true 傳遞給構(gòu)造函數(shù)等效于立即調(diào)用 Set。創(chuàng)建自動(dòng)重置事件的第二種方法如下:

    var auto = new EventWaitHandle (false, EventResetMode.AutoReset);

    在以下示例中,啟動(dòng)了一個(gè)線程,其工作只是等待另一個(gè)線程發(fā)出信號(hào)(參見):

    class BasicWaitHandle
    {
      static EventWaitHandle _waitHandle = new AutoResetEvent (false);
    
      static void Main()
      {
        new Thread (Waiter).Start();
        Thread.Sleep (1000);                  // Pause for a second...
        _waitHandle.Set();                    // Wake up the Waiter.
      }
    
      static void Waiter()
      {
        Console.WriteLine ("Waiting...");
        _waitHandle.WaitOne();                // Wait for notification
        Console.WriteLine ("Notified");
      }
    }
    
    // Output:
    Waiting... (pause) Notified.



    使用 EventWaitHandle 發(fā)出信號(hào)

    如果在沒有線程等待時(shí)調(diào)用 Set,則句柄將保持打開狀態(tài),直到某個(gè)線程調(diào)用 WaitOne。此行為有助于防止標(biāo)題為十字轉(zhuǎn)門的線程與插入票證的線程之間的爭(zhēng)用(“哎呀,插入票證太早了微秒;現(xiàn)在你將不得不無限期地等待!然而,在沒有人等待的旋轉(zhuǎn)柵門上反復(fù)打電話給Set并不允許整個(gè)派對(duì)在到達(dá)時(shí)通過:只有下一個(gè)人被允許通過,額外的門票被“浪費(fèi)”。

    釋放等待句柄

    完成等待句柄后,可以調(diào)用其 Close 方法來釋放 OS 資源。或者,您可以簡(jiǎn)單地刪除對(duì)等待句柄的所有引用,并允許垃圾回收器稍后為您完成作業(yè)(等待句柄實(shí)現(xiàn)終結(jié)器調(diào)用 Close 的處置模式)。這是依賴此備份(可以說)可接受的少數(shù)方案之一,因?yàn)榈却浔牟僮飨到y(tǒng)負(fù)擔(dān)很輕。

    等待句柄在進(jìn)程退出時(shí)自動(dòng)釋放。

    在自動(dòng)重置事件上調(diào)用 Reset 將關(guān)閉旋轉(zhuǎn)柵門(如果它已打開),而無需等待或阻止。

    WaitOne 接受可選的超時(shí)參數(shù),如果等待因超時(shí)而不是獲取信號(hào)而結(jié)束,則返回 false。

    注意

    在超時(shí)為 0 的情況下調(diào)用 WaitOne 將測(cè)試等待句柄是否“打開”,而不會(huì)阻止調(diào)用方。但請(qǐng)記住,這樣做會(huì)重置自動(dòng)重置事件(如果它處于打開狀態(tài))。

    雙向信令

    假設(shè)我們希望主線程連續(xù)三次向工作線程發(fā)出信號(hào)。如果主線程只是快速連續(xù)多次調(diào)用等待句柄上的 Set ,則第二個(gè)或第三個(gè)信號(hào)可能會(huì)丟失,因?yàn)楣ぷ骶€程可能需要一些時(shí)間來處理每個(gè)信號(hào)。

    解決方案是讓主線程等到工作線程準(zhǔn)備就緒后再發(fā)出信號(hào)。我們可以通過使用另一個(gè) 自動(dòng)重置事件 ,如下所示:

    class TwoWaySignaling
    {
      static EventWaitHandle _ready = new AutoResetEvent (false);
      static EventWaitHandle _go = new AutoResetEvent (false);
      static readonly object _locker = new object();
      static string _message;
    
      static void Main()
      {
        new Thread (Work).Start();
    
        _ready.WaitOne();                  // First wait until worker is ready
        lock (_locker) _message = "ooo";
        _go.Set();                         // Tell worker to go
    
        _ready.WaitOne();
        lock (_locker) _message = "ahhh";  // Give the worker another message
        _go.Set();
    
        _ready.WaitOne();
        lock (_locker) _message = null;    // Signal the worker to exit
        _go.Set();
      }
    
      static void Work()
      {
        while (true)
        {
          _ready.Set();                          // Indicate that we're ready
          _go.WaitOne();                         // Wait to be kicked off...
          lock (_locker)
          {
            if (_message == null) return;        // Gracefully exit
            Console.WriteLine (_message);
          }
        }
      }
    }
    
    // Output:
    ooo
    ahhh

    直觀地顯示了此過程。



    雙向信令

    在這里,我們使用空消息來指示工作線程應(yīng)結(jié)束。對(duì)于無限期運(yùn)行的線程,制定退出策略非常重要!

    手動(dòng)重置事件

    正如我們中所描述的,ManualResetEvent的功能類似于一個(gè)簡(jiǎn)單的門。調(diào)用集打開門數(shù)量的線程調(diào)用 WaitOne。呼叫重置將關(guān)閉門。在關(guān)閉的門上調(diào)用 WaitOne 的線程將阻塞;當(dāng)大門下一次打開時(shí),它們將立即全部釋放。除了這些差異之外,手動(dòng)重置事件的功能類似于自動(dòng)重置事件。

    與 AutoResetEvent 一樣,您可以通過兩種方式構(gòu)造 ManualResetEvent:

    var manual1 = new ManualResetEvent (false);
    var manual2 = new EventWaitHandle (false, EventResetMode.ManualReset);

    注意

    還有另一個(gè)版本的ManualResetEvent稱為ManualResetEventSlim。后者針對(duì)較短的等待時(shí)間進(jìn)行了優(yōu)化 - 能夠選擇旋轉(zhuǎn)以進(jìn)行一定次數(shù)的迭代。它還具有更有效的托管實(shí)現(xiàn),并允許通過CancelToken取消等待。ManualResetEventSlim 不子類 WaitHandle ;但是,它公開了一個(gè) WaitHandle 屬性,該屬性在調(diào)用時(shí)返回基于 WaitHandle 的對(duì)象(具有傳統(tǒng)等待句柄的性能配置文件)。

    信令結(jié)構(gòu)和性能

    等待或發(fā)出自動(dòng)重置事件或手動(dòng)重置事件的信號(hào)大約需要一微秒(假設(shè)沒有阻塞)。

    ManualResetEventSlim 和 CountdownEvent 在短等待場(chǎng)景中可以提高 50 倍的速度,因?yàn)樗鼈儾灰蕾嚥僮飨到y(tǒng)并明智地使用旋轉(zhuǎn)結(jié)構(gòu)。但是,在大多數(shù)情況下,信令類本身的開銷不會(huì)造成瓶頸;因此,它很少是一個(gè)考慮因素。

    手動(dòng)重置事件在允許一個(gè)線程取消阻止許多其他線程方面很有用。倒計(jì)時(shí)事件涵蓋了相反的情況。

    倒計(jì)時(shí)事件

    倒計(jì)時(shí)事件允許您等待多個(gè)線程。該類具有高效、完全托管的實(shí)現(xiàn)。若要使用該類,請(qǐng)使用要等待的線程數(shù)或“計(jì)數(shù)”來實(shí)例化它:

    var countdown = new CountdownEvent (3);  // Initialize with "count" of 3.

    呼叫信號(hào)遞減“計(jì)數(shù)”;調(diào)用 等待塊,直到計(jì)數(shù)降至零:

    new Thread (SaySomething).Start ("I am thread 1");
    new Thread (SaySomething).Start ("I am thread 2");
    new Thread (SaySomething).Start ("I am thread 3");
    
    countdown.Wait();   // Blocks until Signal has been called 3 times
    Console.WriteLine ("All threads have finished speaking!");
    
    void SaySomething (object thing)
    {
      Thread.Sleep (1000);
      Console.WriteLine (thing);
      countdown.Signal();
    }

    注意

    有時(shí),通過使用我們?cè)冢≒LINQ 和并行類)中描述的構(gòu)造,可以更輕松地解決 CountdownEvent 有效的問題。

    您可以通過調(diào)用 AddCount 來重新遞增倒計(jì)時(shí)事件的計(jì)數(shù)。但是,如果它已經(jīng)達(dá)到零,則會(huì)引發(fā)異常:您無法通過調(diào)用 AddCount 來“取消信號(hào)”倒計(jì)時(shí)事件。為了防止拋出異常的可能性,您可以改為調(diào)用 TryAddCount ,如果倒計(jì)時(shí)為零,則返回 false。

    要取消向倒計(jì)時(shí)事件發(fā)出信號(hào),請(qǐng)調(diào)用 Reset :這既取消了構(gòu)造的信號(hào),又將其計(jì)數(shù)重置為原始值。

    與 ManualResetEventSlim 一樣,CountdownEvent 公開了一個(gè) WaitHandle 屬性,用于其他類或方法需要基于 的對(duì)象的情況。

    創(chuàng)建跨進(jìn)程事件等待句柄

    EventWaitHandle的構(gòu)造函數(shù)允許創(chuàng)建一個(gè)“命名的”EventWaitHandle,能夠跨多個(gè)進(jìn)程運(yùn)行。名稱只是一個(gè)字符串,它可以是任何不會(huì)無意中與其他人的值沖突的值!如果該名稱已在計(jì)算機(jī)上使用,則會(huì)獲得對(duì)同一基礎(chǔ) EventWaitHandle 的引用;否則,操作系統(tǒng)會(huì)創(chuàng)建一個(gè)新操作系統(tǒng)。下面是一個(gè)示例:

    EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.AutoReset,
                                          @"Global\MyCompany.MyApp.SomeName");

    如果兩個(gè)應(yīng)用程序分別運(yùn)行此代碼,它們將能夠相互發(fā)出信號(hào):等待句柄將跨兩個(gè)進(jìn)程中的所有線程工作。

    命名事件等待句柄僅在 Windows 上可用。

    等待句柄和延續(xù)

    與其等待等待句柄(并阻止線程),不如通過調(diào)用 ThreadPool.RegisterWaitForSingleObject 將“延續(xù)”附加到它。此方法接受在發(fā)出等待句柄信號(hào)時(shí)執(zhí)行的委托:

    var starter = new ManualResetEvent (false);
    
    RegisteredWaitHandle reg = ThreadPool.RegisterWaitForSingleObject
     (starter, Go, "Some Data", -1, true);
    
    Thread.Sleep (5000);
    Console.WriteLine ("Signaling worker...");
    starter.Set();
    Console.ReadLine();
    reg.Unregister (starter);    // Clean up when we’re done.
    
    void Go (object data, bool timedOut)
    {
      Console.WriteLine ("Started - " + data);
      // Perform task...
    }
    
    // Output:
    (5 second delay)
    Signaling worker...
    Started - Some Data

    當(dāng)?shù)却浔l(fā)出信號(hào)(或超時(shí)已過)時(shí),委托將在池線程上運(yùn)行。然后,您應(yīng)該調(diào)用 Unregister 以釋放回調(diào)的非托管句柄。

    除了等待句柄和委托之外,RegisterWaitForSingleObject 還接受一個(gè)“黑盒”對(duì)象,它傳遞給委托方法(很像 ParameterizedThreadStart ),以及以毫秒為單位的超時(shí)(-1 表示沒有超時(shí))和一個(gè)布爾標(biāo)志,指示請(qǐng)求是一次性的還是重復(fù)的。

    注意

    每個(gè)等待句柄只能可靠地調(diào)用一次 RegisterWaitForSingleObject。在同一等待句柄上再次調(diào)用此方法會(huì)導(dǎo)致間歇性故障,即未發(fā)出信號(hào)的等待句柄會(huì)觸發(fā)回調(diào),就像發(fā)出一樣。

    此限制使得(非苗條)等待句柄不適合異步編程。

    WaitAny、WaitAll和SignalAndWait

    除了 Set、WaitOne 和 Reset 方法之外,WaitHandle 類上還有靜態(tài)方法來破解更復(fù)雜的同步螺母。WaitAny 、 方法對(duì)多個(gè)句柄執(zhí)行信令和等待操作。等待句柄可以是不同的類型(包括互斥句柄和信號(hào)量,因?yàn)樗鼈円才缮猿橄蟮?WaitHandle 類)。ManualResetEventSlim 和 CountdownEvent 也可以通過其 WaitHandle 屬性參與這些方法。

    注意

    WaitAll和SignalAndWait與傳統(tǒng)的COM體系結(jié)構(gòu)有著奇怪的聯(lián)系:這些方法要求調(diào)用方位于多線程單元中,這是最不適合互操作性的模型。例如,在此模式下,WPF 或 Windows 窗體應(yīng)用程序的主線程無法與剪貼板交互。我們稍后將討論替代方案。

    WaitHandle.WaitAny 等待等待句柄數(shù)組中的任何一個(gè);WaitHandle.WaitAll 以原子方式等待所有給定的句柄。這意味著,如果您等待兩個(gè)自動(dòng)重置事件:

    • WaitAny永遠(yuǎn)不會(huì)最終“鎖定”這兩個(gè)事件。
    • WaitAll永遠(yuǎn)不會(huì)只“鎖定”一個(gè)事件。

    SignalAndWait 調(diào)用 Set 在一個(gè) WaitHandle 上,然后在另一個(gè) 上調(diào)用 WaitOne。發(fā)出第一個(gè)句柄的信號(hào)后,它會(huì)跳到隊(duì)列的頭部等待第二個(gè)句柄;這有助于它成功(盡管該操作不是真正的原子操作)。您可以將此方法視為將一個(gè)信號(hào)“交換”為另一個(gè)信號(hào),并在一對(duì) EventWaitHandle 上使用它來設(shè)置兩個(gè)線程以在同一時(shí)間點(diǎn)會(huì)合或“相遇”。自動(dòng)重置事件或手動(dòng)重置事件都可以解決問題。第一個(gè)線程執(zhí)行以下命令:

    WaitHandle.SignalAndWait (wh1, wh2);

    第二個(gè)線程執(zhí)行相反的操作:

    WaitHandle.SignalAndWait (wh2, wh1);

    WaitAll 和 SignalAndWait 的替代品

    WaitAll 和 SignalAndWait 不會(huì)在單線程單元中運(yùn)行。幸運(yùn)的是,還有其他選擇。在 SignalAndWait 的情況下,您很少需要它的隊(duì)列跳轉(zhuǎn)語義:例如,在我們的會(huì)合示例中,如果等待句柄僅用于該會(huì)合,則只需在第一個(gè)等待句柄上調(diào)用 Set,然后在另一個(gè)上調(diào)用 WaitOne。在下一節(jié)中,我們將探討實(shí)現(xiàn)線程交合的另一種選擇。

    在 WaitAny 和 WaitAll 的情況下,如果你不需要原子性,你可以使用我們?cè)谏弦还?jié)中編寫的代碼將等待句柄轉(zhuǎn)換為任務(wù),然后使用 Task.WhenAny 和 Task.WhenAll()。

    如果你需要原子性,你可以采用最低級(jí)別的信令方法,并使用監(jiān)視器的等待和脈沖方法自己編寫邏輯。我們將 詳細(xì)描述等待和脈搏。

    屏障類

    Barrier 類實(shí)現(xiàn)了線程,允許許多線程在一個(gè)時(shí)間點(diǎn)會(huì)合(不要與 Thread.MemoryBarrier 混淆)。該類非常快速和高效,并且建立在等待,脈沖和旋轉(zhuǎn)鎖之上。

    使用此類:

    1. 實(shí)例化它,指定應(yīng)該參與會(huì)合的線程數(shù)(您可以稍后通過調(diào)用 AddTParticipants / RemoveParticipants 來更改此設(shè)置)。
    2. 讓每個(gè)線程在想要會(huì)合時(shí)調(diào)用 SignalAndWait。

    實(shí)例化值為 3 的屏障會(huì)導(dǎo)致 SignalAndWait 阻塞,直到該方法被調(diào)用三次。然后它重新開始:再次調(diào)用 SignalAndWait 會(huì)阻止,直到再調(diào)用三次。這使每個(gè)線程與其他每個(gè)線程“同步”。

    在下面的示例中,三個(gè)線程中的每一個(gè)都寫入數(shù)字 0 到 4,同時(shí)與其他線程保持同步:

    var barrier = new Barrier (3);
    
    new Thread (Speak).Start();
    new Thread (Speak).Start();
    new Thread (Speak).Start();
    
    void Speak()
    {
      for (int i = 0; i < 5; i++)
      {
        Console.Write (i + " ");
        barrier.SignalAndWait();
      }
    }
    
    OUTPUT:  0 0 0 1 1 1 2 2 2 3 3 3 4 4 4

    屏障的一個(gè)非常有用的功能是,您還可以在構(gòu)造它時(shí)指定后期。這是一個(gè)委托,在 SignalAndWait 被調(diào)用 次之后運(yùn)行,但在線程被解鎖運(yùn)行(如圖 中的陰影區(qū)域所示)。在我們的示例中,如果我們按如下方式實(shí)例化我們的屏障

    static Barrier _barrier = new Barrier (3, barrier => Console.WriteLine());

    輸出是這樣的:

    0 0 0 
    1 1 1 
    2 2 2 
    3 3 3 
    4 4 4



    障礙

    后期操作對(duì)于合并來自每個(gè)工作線程的數(shù)據(jù)非常有用。它不需要擔(dān)心搶占,因?yàn)樵谒鏊氖虑闀r(shí),所有工作線程都會(huì)被阻止。

    延遲初始化

    線程處理中常見的問題是如何以線程安全的方式延遲初始化共享字段。當(dāng)您有一個(gè)構(gòu)造成本很高的字段時(shí),就會(huì)出現(xiàn)

    class Foo
    {
      public readonly Expensive Expensive = new Expensive();
      ...
    }
    class Expensive {  /* Suppose this is expensive to construct */  }

    此代碼的問題在于,實(shí)例化 Foo 會(huì)產(chǎn)生實(shí)例化 Expensive 的性能成本 — 無論是否訪問過 Expensive 字段。顯而易見的答案是構(gòu)建實(shí)例:

    class Foo
    {
      Expensive _expensive;
      public Expensive Expensive         // Lazily instantiate Expensive
      {
        get
        {
          if (_expensive == null) _expensive = new Expensive();
          return _expensive;
        }
      }
      ...
    }

    那么問題來了,這是線程安全的嗎?除了我們?cè)跊]有內(nèi)存屏障的情況下訪問鎖之外的_expensive之外,請(qǐng)考慮如果兩個(gè)線程同時(shí)訪問此屬性會(huì)發(fā)生什么。它們都可以滿足 if 語句的謂詞,并且每個(gè)線程最終都會(huì)得到 Expensive 實(shí)例。因?yàn)檫@可能會(huì)導(dǎo)致細(xì)微的錯(cuò)誤,所以一般來說,我們會(huì)說這段代碼不是線程安全的。

    該問題的解決方案是鎖定檢查和初始化對(duì)象:

    Expensive _expensive;
    readonly object _expenseLock = new object();
    
    public Expensive Expensive
    {
      get
      {
        lock (_expenseLock)
        {
          if (_expensive == null) _expensive = new Expensive();
          return _expensive;
        }
      }
    }

    懶惰<T>

    Lazy<T> 類可用于幫助延遲初始化。如果使用參數(shù) true 實(shí)例化,它實(shí)現(xiàn)了剛才描述的線程安全初始化模式。

    注意

    Lazy<T>實(shí)際上實(shí)現(xiàn)了這種模式的微優(yōu)化版本,稱為。仔細(xì)檢查鎖定執(zhí)行額外的易失性讀取,以避免在對(duì)象已初始化時(shí)獲取鎖定的成本。

    要使用 Lazy<T> ,使用值工廠委托實(shí)例化類,該委托告訴它如何初始化新值,并且參數(shù)為 true 。然后,通過 Value 屬性訪問其值:

    Lazy<Expensive> _expensive = new Lazy<Expensive>
      (() => new Expensive(), true);
    
    public Expensive Expensive { get { return _expensive.Value; } }

    如果將 false 傳遞到 Lazy<T> 的構(gòu)造函數(shù)中,它將實(shí)現(xiàn)我們?cè)诒竟?jié)開頭描述的線程不安全的延遲初始化模式 — 當(dāng)您想在單線程上下文中使用 Lazy<T> 時(shí),這是有意義的。

    延遲初始值設(shè)定項(xiàng)

    LazyInitializer 是一個(gè)靜態(tài)類,其工作原理與 Lazy<T 完全相同>除了:

    • 其功能通過靜態(tài)方法公開,該方法直接在你自己類型的字段上運(yùn)行。這可以防止一定程度的間接性,從而在需要極端優(yōu)化的情況下提高性能。
    • 它提供了另一種初始化模式,其中多個(gè)線程可以競(jìng)相初始化。

    要使用 LazyInitializer ,請(qǐng)?jiān)谠L問字段之前調(diào)用 EnsureInitialized,傳遞對(duì)字段和工廠委托的引用:

    Expensive _expensive;
    public Expensive Expensive
    { 
      get          // Implement double-checked locking
      { 
        LazyInitializer.EnsureInitialized (ref _expensive,
                                          () => new Expensive());
        return _expensive;
      }
    }

    您還可以傳入另一個(gè)參數(shù)來請(qǐng)求競(jìng)爭(zhēng)線程初始化。這聽起來類似于我們最初的線程不安全示例,只是第一個(gè)完成的線程總是獲勝,因此您最終只有一個(gè)實(shí)例。這種技術(shù)的優(yōu)點(diǎn)是它比雙重檢查鎖定更快(在多核上),因?yàn)樗梢允褂梦覀冊(cè)?的“非阻塞同步”和“延遲初始化”中描述的高級(jí)技術(shù)完全在沒有鎖的情況下實(shí)現(xiàn)。這是一個(gè)極端的(很少需要的)優(yōu)化,需要付出代價(jià):

    • 當(dāng)更多的線程競(jìng)相初始化時(shí),它會(huì)比你擁有的內(nèi)核更慢。
    • 它可能會(huì)浪費(fèi)執(zhí)行冗余初始化的 CPU 資源。
    • 初始化邏輯必須是線程安全的(在這種情況下,例如,如果 Expensive 的構(gòu)造函數(shù)寫入靜態(tài)字段,這將是線程不安全的)。
    • 如果初始值設(shè)定項(xiàng)實(shí)例化需要處置的對(duì)象,則如果沒有其他邏輯,將不會(huì)釋放“浪費(fèi)”的對(duì)象。

    線程本地存儲(chǔ)

    本章的大部分內(nèi)容都集中在同步構(gòu)造以及線程同時(shí)訪問相同數(shù)據(jù)所產(chǎn)生的問題。但是,有時(shí)您希望保持?jǐn)?shù)據(jù)隔離,確保每個(gè)線程都有單獨(dú)的副本。局部變量正是實(shí)現(xiàn)這一點(diǎn)的,但它們僅對(duì)瞬態(tài)數(shù)據(jù)有用。

    解決方案是。您可能很難想到一個(gè)要求:您希望與線程隔離的數(shù)據(jù)本質(zhì)上往往是暫時(shí)的。它的主要應(yīng)用是存儲(chǔ)“帶外”數(shù)據(jù),即支持執(zhí)行路徑的基礎(chǔ)結(jié)構(gòu)(如消息傳遞、事務(wù)和安全令牌)的數(shù)據(jù)。在方法參數(shù)中傳遞此類數(shù)據(jù)可能很笨拙,并且可能會(huì)疏遠(yuǎn)除您自己的方法之外的所有方法;將此類信息存儲(chǔ)在普通靜態(tài)字段中意味著在所有線程之間共享它。

    線程本地存儲(chǔ)也可用于優(yōu)化并行代碼。它允許每個(gè)線程以獨(dú)占方式訪問其自己的線程不安全對(duì)象版本,而無需鎖,也無需在方法調(diào)用之間重建該對(duì)象。

    有四種方法可以實(shí)現(xiàn)線程本地存儲(chǔ)。我們將在以下小節(jié)中查看它們。

    [線程靜態(tài)]

    線程本地存儲(chǔ)的最簡(jiǎn)單方法是使用 ThreadStatic 屬性標(biāo)記靜態(tài)字段:

    [ThreadStatic] static int _x;

    然后,每個(gè)線程都會(huì)看到 _x 的單獨(dú)副本。

    不幸的是,[ThreadStatic] 不適用于實(shí)例字段(它什么都不做);它也不能很好地與字段初始值設(shè)定項(xiàng)配合使用 — 它們僅在靜態(tài)構(gòu)造函數(shù)執(zhí)行時(shí)正在運(yùn)行的線程上執(zhí)行。如果您需要使用實(shí)例字段(或從非默認(rèn)值開始),ThreadLocal<T> 提供了更好的選擇。

    ThreadLocal<T>

    ThreadLocal<T> 為靜態(tài)字段和實(shí)例字段提供線程本地存儲(chǔ),并允許您指定默認(rèn)值。

    下面介紹如何創(chuàng)建 ThreadLocal<int>,每個(gè)線程的默認(rèn)值為 3:

    static ThreadLocal<int> _x = new ThreadLocal<int> (() => 3);

    然后_x 的 Value 屬性獲取或設(shè)置其線程本地值。使用 ThreadLocal 的一個(gè)好處是可以延遲計(jì)算值:工廠函數(shù)在第一次調(diào)用時(shí)(針對(duì)每個(gè)線程)進(jìn)行評(píng)估。

    線程本地<T> 和實(shí)例字段

    ThreadLocal<T> 對(duì)于實(shí)例字段和捕獲的局部變量也很有用。例如,考慮在多線程環(huán)境中生成隨機(jī)數(shù)的問題。Random 類不是線程安全的,因此我們必須使用 Random 鎖定(限制并發(fā)性)或?yàn)槊總€(gè)線程生成一個(gè)單獨(dú)的 Random 對(duì)象。ThreadLocal<T>使后者變得容易:

    var localRandom = new ThreadLocal<Random>(() => new Random());
    Console.WriteLine (localRandom.Value.Next());

    我們用于創(chuàng)建 Random 對(duì)象的工廠函數(shù)有點(diǎn)簡(jiǎn)單,因?yàn)?Random 的無參數(shù)構(gòu)造函數(shù)依賴于隨機(jī)數(shù)種子的系統(tǒng)時(shí)鐘。對(duì)于在 ~10 毫秒內(nèi)創(chuàng)建的兩個(gè)隨機(jī)對(duì)象,這可能相同。這是修復(fù)它的一種方法:

    var localRandom = new ThreadLocal<Random>
     ( () => new Random (Guid.NewGuid().GetHashCode()) );

    我們?cè)谥惺褂盟▍⒁娭械牟⑿衅磳憴z查示例)。

    GetData 和 SetData

    第三種方法是在 Thread 類中使用兩個(gè)方法:GetData 和 SetData。它們將數(shù)據(jù)存儲(chǔ)在特定于線程的“插槽”中。Thread.GetData 從線程的獨(dú)立數(shù)據(jù)存儲(chǔ)中讀取;Thread.SetData 寫入它。這兩種方法都需要一個(gè) LocalDataStoreSlot 對(duì)象來標(biāo)識(shí)槽。您可以在所有線程中使用相同的槽,它們?nèi)詫@得單獨(dú)的值。下面是一個(gè)示例:

    class Test
    {
      // The same LocalDataStoreSlot object can be used across all threads.
      LocalDataStoreSlot _secSlot = Thread.GetNamedDataSlot ("securityLevel");
    
      // This property has a separate value on each thread.
      int SecurityLevel
      {
        get
        {
          object data = Thread.GetData (_secSlot);
          return data == null ? 0 : (int) data;    // null == uninitialized
        }
        set { Thread.SetData (_secSlot, value); }
      }
      ...

    在本例中,我們稱為 Thread.GetNamedDataSlot ,它創(chuàng)建一個(gè)命名槽 — 這允許在應(yīng)用程序中共享該槽。或者,您可以使用未命名的插槽自己控制插槽的作用域,通過調(diào)用 Thread.AllocateDataSlot 獲得:

    class Test
    {
      LocalDataStoreSlot _secSlot = Thread.AllocateDataSlot();
      ...

    Thread.FreeNamedDataSlot 將在所有線程中釋放命名數(shù)據(jù)槽,但只有在對(duì)該 LocalDataStoreSlot 的所有引用都退出范圍并被垃圾回收后。這可確保線程不會(huì)從其腳下拉出數(shù)據(jù)槽,只要它們?cè)谛枰蹠r(shí)保留對(duì)相應(yīng) LocalDataStoreSlot 對(duì)象的引用即可。

    AsyncLocal<T>

    到目前為止,我們討論的線程本地存儲(chǔ)方法與異步函數(shù)不兼容,因?yàn)樵?await 之后,可以在不同的線程上恢復(fù)執(zhí)行。AsyncLocal<T> 類通過在 await 中保留其值來解決此問題:

    static AsyncLocal<string> _asyncLocalTest = new AsyncLocal<string>();
    
    async void Main()
    {
      _asyncLocalTest.Value = "test";  
      await Task.Delay (1000);  
      // The following works even if we come back on another thread:
      Console.WriteLine (_asyncLocalTest.Value);   // test
    }

    AsyncLocal<T> 仍然能夠?qū)⒉僮鞣謩e在單獨(dú)的線程上啟動(dòng),無論是由 Thread 發(fā)起的。啟動(dòng)或任務(wù) 。跑。下面寫“一一”和“二二”:

    static AsyncLocal<string> _asyncLocalTest = new AsyncLocal<string>();
    
    void Main()
    {
      // Call Test twice on two concurrent threads:
      new Thread (() => Test ("one")).Start();
      new Thread (() => Test ("two")).Start();
    }
    
    async void Test (string value)
    {
      _asyncLocalTest.Value = value;
      await Task.Delay (1000);
      Console.WriteLine (value + " " + _asyncLocalTest.Value);
    }

    AsyncLocal<T> 有一個(gè)有趣且獨(dú)特的細(xì)微差別:如果 AsyncLocal<T> 對(duì)象在啟動(dòng)線程時(shí)已經(jīng)有一個(gè)值,則新線程將“繼承”該值:

    static AsyncLocal<string> _asyncLocalTest = new AsyncLocal<string>();
    
    void Main()
    {
      _asyncLocalTest.Value = "test";
      new Thread (AnotherMethod).Start();
    }
    
    void AnotherMethod() => Console.WriteLine (_asyncLocalTest.Value);  // test

    但是,新線程會(huì)獲取該因此它所做的任何更改都不會(huì)影響原始值:

    static AsyncLocal<string> _asyncLocalTest = new AsyncLocal<string>();
    
    void Main()
    {
      _asyncLocalTest.Value = "test";
      var t = new Thread (AnotherMethod);
      t.Start(); t.Join();
      Console.WriteLine (_asyncLocalTest.Value);   // test  (not ha-ha!)
    }
    
    void AnotherMethod() => _asyncLocalTest.Value = "ha-ha!";

    請(qǐng)記住,新線程將獲取該值的拷貝。因此,如果要將 Async<string> 替換為 Async<StringBuilder> 或 Async<List<string>> ,新線程可以清除 StringBuilder 或向 List<string 添加/刪除項(xiàng)目> ,這將影響原始線程。

    定時(shí)器

    如果您需要定期重復(fù)執(zhí)行某個(gè)方法,最簡(jiǎn)單的方法是使用。與以下技術(shù)相比,計(jì)時(shí)器在使用內(nèi)存和資源方面既方便又高效:

    new Thread (delegate() {
                             while (enabled)
                             {
                               DoSomeAction();
                               Thread.Sleep (TimeSpan.FromHours (24));
                             }
                           }).Start();

    這不僅會(huì)永久占用線程資源,而且無需額外的編碼,DoSomeAction 將在每天晚些時(shí)候發(fā)生。計(jì)時(shí)器解決了。

    .NET 提供五個(gè)計(jì)時(shí)器。其中兩個(gè)是通用多線程計(jì)時(shí)器:

    • 系統(tǒng)線程.計(jì)時(shí)器
    • 系統(tǒng).定時(shí)器.定時(shí)器

    另外兩個(gè)是專用單線程計(jì)時(shí)器:

    • System.Windows.Forms.Timer (Windows Forms timer)
    • System.Windows.Threading.DispatcherTimer (WPF timer)

    多線程定時(shí)器功能更強(qiáng)大、更準(zhǔn)確、更靈活;單線程計(jì)時(shí)器更安全、更方便地運(yùn)行更新 Windows 窗體控件或 WPF 元素的簡(jiǎn)單任務(wù)。

    最后,從 .NET 6 開始,有 周期計(jì)時(shí)器 ,我們將首先介紹它。

    周期計(jì)時(shí)器

    周期計(jì)時(shí)器并不是真正的計(jì)時(shí)器;這是一個(gè)幫助異步循環(huán)的類。重要的是要考慮到,自從異步和等待的出現(xiàn)以來,傳統(tǒng)的計(jì)時(shí)器通常不是必需的。相反,以下模式效果很好:

    StartPeriodicOperation();
    
    async void StartPeriodicOperation()
    {
      while (true)
      {
        await Task.Delay (1000);
        Console.WriteLine ("Tick");   // Do some action
      }
     }

    注意

    如果從 UI 線程調(diào)用 StartPeriodicOperation,它將表現(xiàn)為單線程計(jì)時(shí)器,因?yàn)?await 將始終返回相同的同步上下文。

    您只需添加 .ConfigureAwait(false) to the await.

    PeriodicTimer 是一個(gè)簡(jiǎn)化此模式的類:

    var timer = new PeriodicTimer (TimeSpan.FromSeconds (1));
    StartPeriodicOperation();
    // Optionally dispose timer when you want to stop looping.
    
    async void StartPeriodicOperation()
    {
      while (await timer.WaitForNextTickAsync())
        Console.WriteLine ("Tick");    // Do some action
    }

    PeriodicTimer 還允許您通過釋放計(jì)時(shí)器實(shí)例來停止計(jì)時(shí)器。這會(huì)導(dǎo)致 WaitForNextTickAsync 返回 false,從而允許循環(huán)結(jié)束。

    多線程計(jì)時(shí)器

    System.Threading.Timer 是最簡(jiǎn)單的多線程計(jì)時(shí)器:它只有一個(gè)構(gòu)造函數(shù)和兩個(gè)方法(極簡(jiǎn)主義者和書籍作者都很高興!在下面的示例中,計(jì)時(shí)器調(diào)用 Tick 方法,該方法寫入“tick...”五秒鐘后,然后每隔一秒,直到用戶按 Enter :

    using System;
    using System.Threading;
    
    // First interval = 5000ms; subsequent intervals = 1000ms
    Timer tmr = new Timer (Tick, "tick...", 5000, 1000);
    Console.ReadLine();
    tmr.Dispose();         // This both stops the timer and cleans up.
    
    void Tick (object data)
    {
      // This runs on a pooled thread
      Console.WriteLine (data);          // Writes "tick..."
    }

    注意

    有關(guān)釋放多線程計(jì)時(shí)器的討論,請(qǐng)參閱中的

    稍后可以通過調(diào)用計(jì)時(shí)器的 Change 方法更改計(jì)時(shí)器的間隔。如果希望計(jì)時(shí)器只觸發(fā)一次,請(qǐng)?jiān)跇?gòu)造函數(shù)的最后一個(gè)參數(shù)中指定 Timeout.Infinite 。

    .NET 在 System.Timers 命名空間中提供了另一個(gè)同名的計(jì)時(shí)器類。這簡(jiǎn)單地包裝了System.Threading.Timer ,在使用相同的底層引擎時(shí)提供了額外的便利。以下是其附加功能的摘要:

    • 一個(gè) IComponent 實(shí)現(xiàn),允許它位于 Visual Studio 的設(shè)計(jì)器組件托盤中
    • Interval 屬性而不是 Change 方法
    • 已用而不是回調(diào)委托
    • 一個(gè) Enabled 屬性,用于啟動(dòng)和停止計(jì)時(shí)器(其默認(rèn)值為 false)
    • 啟動(dòng)和停止方法,以防您對(duì)已啟用感到困惑
    • 用于指示定期事件的自動(dòng)重置標(biāo)志(默認(rèn)值為 true)
    • 具有 Invoke 和 BeginInvoke 方法的 SynchronizingObject 屬性,用于安全地調(diào)用 WPF 元素和 Windows 窗體控件上的方法

    下面是一個(gè)示例:

    using System;
    using System.Timers;          // Timers namespace rather than Threading
    
    var tmr = new Timer();        // Doesn't require any args
    tmr.Interval = 500;
    tmr.Elapsed += tmr_Elapsed;   // Uses an event instead of a delegate
    tmr.Start();                  // Start the timer
    Console.ReadLine();
    tmr.Stop();                   // Stop the timer
    Console.ReadLine();
    tmr.Start();                  // Restart the timer
    Console.ReadLine();
    tmr.Dispose();                // Permanently stop the timer
    
    void tmr_Elapsed (object sender, EventArgs e)
      => Console.WriteLine ("Tick");

    多線程計(jì)時(shí)器使用線程池允許幾個(gè)線程為多個(gè)計(jì)時(shí)器提供服務(wù)。這意味著每次調(diào)用回調(diào)方法或 Elapsed 事件時(shí),它可以在不同的線程上觸發(fā)。此外,“已用”事件始終(大約)按時(shí)觸發(fā),而不管上一個(gè)“已用”事件是否完成執(zhí)行。因此,回調(diào)或事件處理程序必須是線程安全的。

    多線程計(jì)時(shí)器的精度取決于操作系統(tǒng),通常在 10 到 20 毫秒的區(qū)域內(nèi)。如果需要更高的精度,可以使用本機(jī)互操作并調(diào)用 Windows 多媒體計(jì)時(shí)器。這具有低至一毫秒的精度,并以 定義。首先調(diào)用timeBeginPeriod通知操作系統(tǒng)你需要高定時(shí)精度,然后調(diào)用timeSetEvent啟動(dòng)多媒體定時(shí)器。完成后,調(diào)用 timeKillEvent 停止計(jì)時(shí)器,調(diào)用 timeEndPeriod 通知操作系統(tǒng)不再需要高計(jì)時(shí)精度。演示了使用P/Invoke調(diào)用外部方法。您可以通過搜索關(guān)鍵字在互聯(lián)網(wǎng)上找到使用多媒體計(jì)時(shí)器的完整示例。

    單線程定時(shí)器

    .NET 提供了旨在消除 WPF 和 Windows 窗體應(yīng)用程序的線程安全問題的計(jì)時(shí)器:

    • System.Windows.Threading.DispatcherTimer (WPF)
    • System.Windows.Forms.Timer (Windows Forms)

    注意

    單線程計(jì)時(shí)器不是為在各自的環(huán)境之外工作而設(shè)計(jì)的。例如,如果在 Windows 服務(wù)應(yīng)用程序中使用 Windows 窗體計(jì)時(shí)器,則計(jì)時(shí)器事件將不會(huì)觸發(fā)!

    兩者都類似于它們公開的成員中的 System.Timers.Timer — 間隔 、 開始 和 停止 (以及 Tick,相當(dāng)于 已過 )— 并且以類似的方式使用。但是,它們?cè)趦?nèi)部的工作方式上有所不同。它們不是在池線程上觸發(fā)計(jì)時(shí)器事件,而是將事件發(fā)布到 WPF 或 Windows 窗體消息循環(huán)。這意味著 Tick 事件始終在最初創(chuàng)建計(jì)時(shí)器的同一線程上觸發(fā),在普通應(yīng)用程序中,該線程是用于管理所有用戶界面元素和控件的同一線程。這有很多:

    • 您可以忘記線程安全。
    • 在上一個(gè)即時(shí)報(bào)價(jià)完成處理之前,新的即時(shí)報(bào)價(jià)永遠(yuǎn)不會(huì)觸發(fā)。
    • 您可以直接從 Tick 事件處理代碼更新用戶界面元素和控件,而無需調(diào)用 Control.BeginInvoke 或 Dispatcher.BeginInvoke 。

    因此,使用這些計(jì)時(shí)器的程序并不是真正的多線程:你最終會(huì)得到與中描述的相同類型的偽并發(fā),即在UI線程上執(zhí)行的異步函數(shù)。一個(gè)線程為所有計(jì)時(shí)器以及處理 UI 事件提供服務(wù)。這意味著 Tick 事件處理程序必須快速執(zhí)行,否則 UI 將無響應(yīng)。

    這使得 WPF 和 Windows 窗體計(jì)時(shí)器適用于小型作業(yè),通常更新 UI 的某些方面(例如,時(shí)鐘或倒計(jì)時(shí)顯示)。

    在精度方面,單線程計(jì)時(shí)器類似于多線程計(jì)時(shí)器(數(shù)十毫秒),盡管它們通常不太,因?yàn)樗鼈兛赡軙?huì)在處理其他 UI 請(qǐng)求(或其他計(jì)時(shí)器事件)時(shí)延遲。

    HDC2024華為重磅發(fā)布全自研操作系統(tǒng)內(nèi)核—鴻蒙內(nèi)核,鴻蒙內(nèi)核替換Linux內(nèi)核成為HarmonyOS NEXT穩(wěn)定流暢新基座。鴻蒙內(nèi)核具備更彈性、更流暢、更安全三大特征,性能超越Linux內(nèi)核10.7%。

    鴻蒙內(nèi)核更彈性:元OS架構(gòu),性能安全雙收益

    萬物智聯(lián)時(shí)代的終端設(shè)備呼喚面向全場(chǎng)景的、更流暢、更安全的操作系統(tǒng)內(nèi)核,傳統(tǒng)終端內(nèi)核雖有豐富的生態(tài),但在彈性、安全可靠、可演進(jìn)性方面存在挑戰(zhàn),其深度耦合的架構(gòu)也對(duì)創(chuàng)新造成掣肘。

    鴻蒙內(nèi)核在經(jīng)典微內(nèi)核架構(gòu)基礎(chǔ)上進(jìn)一步創(chuàng)新元OS架構(gòu)[1],解決微內(nèi)核性能挑戰(zhàn),同時(shí)比混合內(nèi)核更加靈活,可彈性滿足多樣化場(chǎng)景需求。

    元OS可上可下、可分可合的靈活架構(gòu):鴻蒙內(nèi)核基于組件理論模型實(shí)現(xiàn)功能特性細(xì)粒度解耦,并可根據(jù)不同場(chǎng)景的功能、性能、安全等差異化需求進(jìn)行貼身適配及靈活組合部署,分則提升安全可靠,合則優(yōu)化性能功耗,結(jié)合軟硬件協(xié)同的輕量級(jí)隔離機(jī)制,實(shí)現(xiàn)性能、安全、時(shí)延的多重收益。鴻蒙內(nèi)核技術(shù)創(chuàng)新更多內(nèi)容將發(fā)表在操作系統(tǒng)全球頂級(jí)學(xué)術(shù)會(huì)議OSDI '24 [2]。

    鴻蒙內(nèi)核更流暢:全棧協(xié)同,流暢度遠(yuǎn)超傳統(tǒng)內(nèi)核

    鴻蒙內(nèi)核圍繞調(diào)度、內(nèi)存、進(jìn)程通信等核心能力進(jìn)行創(chuàng)新,并通過全棧協(xié)同提升效率。

    感知協(xié)同QoS調(diào)度:為解決傳統(tǒng)內(nèi)核調(diào)度在終端領(lǐng)域的三大問題(喚醒時(shí)延不穩(wěn)定、優(yōu)先級(jí)反轉(zhuǎn)、負(fù)載均衡控制不準(zhǔn)確),鴻蒙內(nèi)核打造感知協(xié)同QoS調(diào)度技術(shù),賦能移動(dòng)應(yīng)用進(jìn)行資源管理架構(gòu)升級(jí),提升關(guān)鍵場(chǎng)景幀率和流暢性體驗(yàn)。

    混合動(dòng)態(tài)大頁:拍照、游戲、應(yīng)用啟動(dòng)等重載場(chǎng)景需大量?jī)?nèi)存,傳統(tǒng)內(nèi)存管理采用4KB固定小頁方式效率低。鴻蒙內(nèi)核打造混合動(dòng)態(tài)大頁技術(shù),可根據(jù)場(chǎng)景動(dòng)態(tài)分配4/16/64KB頁面,數(shù)倍提升內(nèi)存管理效率;同時(shí)通過內(nèi)核與應(yīng)用協(xié)同實(shí)現(xiàn)類型感知的內(nèi)存管理,大幅提升內(nèi)存利用率。

    極簡(jiǎn)進(jìn)程間通信(IPC):終端系統(tǒng)服務(wù)眾多,通信協(xié)同頻率高,鴻蒙內(nèi)核原生的極簡(jiǎn)IPC機(jī)制解決了路徑冗長(zhǎng)、調(diào)度耦合等缺點(diǎn),多跳變一跳,顯著降低IPC開銷,優(yōu)化關(guān)鍵路徑時(shí)延。

    鴻蒙內(nèi)核更安全:架構(gòu)安全,獲業(yè)界多個(gè)頂級(jí)認(rèn)證證書

    鴻蒙內(nèi)核卓越的安全能力獲得業(yè)界高度認(rèn)可:

    鴻蒙內(nèi)核具有精簡(jiǎn)的可信基、增強(qiáng)的隔離策略及縱深安全防御機(jī)制,核心組件經(jīng)形式化證明,獲得全球首個(gè)通用操作系統(tǒng)內(nèi)核CC EAL6+ 認(rèn)證,并獲得最高等級(jí)車規(guī)安全認(rèn)證(ISO 26262 ASIL D)和工業(yè)軟件領(lǐng)域高等級(jí)功能安全認(rèn)證(IEC 61508 SIL 3)。此外,鴻蒙內(nèi)核還獲得中國(guó)信通院自主成熟度最高等級(jí)認(rèn)證。

    [1] Embracing connected intelligence with the YuanOS architecture: one OS kit for all

    [2] Microkernel Goes General: Performance and Compatibility in the HongMeng Production Microkernel(2024年7月10日隨OSDI '24會(huì)議召開發(fā)表)

網(wǎng)站首頁   |    關(guān)于我們   |    公司新聞   |    產(chǎn)品方案   |    用戶案例   |    售后服務(wù)   |    合作伙伴   |    人才招聘   |   

友情鏈接: 餐飲加盟

地址:北京市海淀區(qū)    電話:010-     郵箱:@126.com

備案號(hào):冀ICP備2024067069號(hào)-3 北京科技有限公司版權(quán)所有