線程同步的情景之一

 

  從本篇文章開始,我將陸續介紹多線程中會遇到的三種狀況。 數組

 

 

  情景一:此茅坑有主了安全

 

  大錘:「我擦,竟然一個茅坑有兩我的在用。」多線程

  大錘:「啊,忍不住了,一塊兒擠擠吧~~~」併發

  叫獸:「舒坦了,先走了。」函數

  叫獸按下了沖水開關.... "嘩啦啦....."post

  大錘:「你妹啊,衝什麼水啊,衝得我一身 shit 」性能

 

解決方案:爲了解決這種混亂的狀況,管理員給茅坑加了道門,一次只容許一我的使用,其餘人只能在外面等待。並且只要有人佔着,就算不拉屎,其餘人也只能乖乖排隊。spa

問題抽象:當某一資源可能同時被多個線程讀取和修改時,資源的狀態將變得難以預料。操作系統

線程同步方案:Interlocked、lock、Moniter、SpinLock、ReadWriteLockSlim、Mutex線程

方案特性:除全部者外,其餘人無條件等待;先到先得(誰先進茅坑,誰先用,沒有前後順序)

 

各方案間的區別(關於如何使用每種方案,不少文章和書籍都有介紹,就再也不一一贅述了。)

這些方案從它們各自的實現方式可分爲三種:用戶模式構造、內核模式構造 和 混合模式構造。

應該儘可能使用用戶模式構造,它們的速度要顯著快於內核模式的構造。這是由於它們使用了特殊 CPU 指令來協調線程。這意味着協調是在硬件中發生的(因此才這麼快)。它們有一個缺點:只有 Windows 操做系統內核才能中止一個線程的運行(以免浪費 CPU 時間)。因此,一個線程想要取得一個資源但又暫時取不到,它會一直在用戶模式中運行。這可能浪費大量 CPU 時間。

內核模式的構造是由 Windows 操做系統自身提供的。因此,它們要求你在應用程序的線程中調用在操做系統內核中實現的函數。將線程從用戶模式切換爲內核模式(或相反)會招致巨大的性能損失,這正是爲何應該避免使用內核模式構造的緣由。而後,它們有一個重要的優勢:一個線程使用一個內核模式的構造獲取一個由其它線程擁有的資源時,Windows會阻塞線程,使它再也不浪費 CPU 時間。而後,當資源變得可用時,Windows 會恢復線程,容許它訪問資源。

---- 《CLR via C# (第 3 版)》 P706

  上面這段話摘自《CLR via C#》,各別用詞稍微調整了下以便於理解。簡單來講,用戶模式會經過在 CPU 中不斷的執行某些指令來達到阻塞線程的效果(想像一下一直執行 while(true); 的樣子),而內核模式則是實實在在的把線程的執行給中止了,CPU 不會再去調度這個線程。混合模式,就不用說了,是二者的結合。

  那何時該用什麼模式的構造呢?對於短期的阻塞,選擇用戶模式;長時間的阻塞,選擇內核模式;阻塞時間不定的,選擇混合模式。

 

用戶模式(user-mode)

  Interlocked 保證的是原子性,其原子操做包括 「遞增」、「遞減」、「相加」、「交換」 。之因此把它也納入情景一,是由於它經過原子操做確保一個資源在 「讀取後,寫入前」 不會有其它線程中斷它的執行,從而保證了資源的獨佔使用。

  優勢:速度最快,且單次操做阻塞時間短。

  缺點:可執行的操做有限。

  

  SpinLock 自旋鎖,在 .Net 4.0 的時候引入。自旋的意思就是自個兒在原地旋轉,以此來佔用 CPU 時間。說白了就是相似 「while(狀態是否可用); 」,若是狀態不可用,則一直循環,直到狀態可用爲止。能夠用 Interlocked 來實現 SpinLock 的效果:

    //參考 Clr via C#
    struct MySpinLock
    {
        int _lock;
 
        public void Enter()
        {
            //第一個線程進來的時候,Exchange 返回0,while 退出。其它線程進來,都返回1
            while (Interlocked.Exchange(ref _lock, 1) == 1) ;
        }
 
        public void Exit()
        {
            Interlocked.Exchange(ref _lock, 0);
        }
    }

  優勢:速度快,能夠用於各類操做。

  缺點:若是操做須要很長時間,將會嚴重浪費 CPU 時間。在單核的處理器中使用該方式,可能形成死鎖。由於若是加鎖的線程優先級低於阻塞的線程,那可能很長一段時間都沒法被調度到CPU上,這樣就沒法解鎖。

 

內核模式(kernal-mode)

  Mutex 能夠跨進程保證資源的獨佔使用,經過 WaitOne 來獲取鎖,ReleaseMutex 釋放鎖(使用哪一個線程執行的 WaitOne,只能由該線程 ReleaseMutex)。它與後面要講到的 「Event」 都來自於同一個父類 WaitHandle。這是一個抽象類,包裝了 Windows 操做系統的內核對象句柄。

  主要用於:限制應用程序只能啓動一次。如 Sql Server、360安全衛士。

 

  代碼示例:

    [STAThread]
    static void Main()
    {
        bool loaded = false;
        Mutex mutex = new Mutex(false, "SINGILE", out loaded);
 
        if (loaded)
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
        else
        {
            Application.Exit();
        }          
    }

  優勢:容許遞歸使用,能夠跨進程使用

  缺點:速度最慢(不只是由於會在內核模式與用戶模式間進行切換,形成性能的損失;也由於相對於 Event,它提供了遞歸使用等高級的功能,這致使它比其它結構都要複雜)

 

混合模式(hybrid-mode)

  Moniter 方式經過調用靜態方法 Enter、Exit 來實現對共享資源或代碼段的獨佔使用,是 .Net 領域中問世最先的一種線程同步機制。咱們都知道每一個引用類型在堆中都會包含兩個特殊的字段:同步塊索引 和 類型對象指針。而使用 Moniter.Enter 實際就會去操做同步塊索引,讓它指向堆中的同步塊數組;Mointer.Exit 則會從新將同步塊索引置爲 -1。

  優勢:速度還行,介於內核模式和用戶模式之間;支持遞歸使用。

  缺點:會把全部操做(讀或寫)該資源的線程都阻塞,而當系統中讀線程的數量遠遠多於寫線程的時候,頗有可能出現同一時刻只有多個讀線程,這個時候阻塞的行爲就顯得多餘了。

 

 

  Lock 是 C# 的語法糖,經過查看 IL 代碼能夠知道,它最終將被解釋爲 Moniter.Enter 和 Moniter.Exit。下面是 C# 4.0 代碼的 IL。

  經過上面的 IL,能夠明確的看到 Moniter.Exit 被放置在 finally 塊中,這樣保證了鎖最終將被正確釋放(避免了可能發生的死鎖)。但有一點值得注意的是,若是代碼塊中拋出了異常,儘管能夠保證鎖被釋放,但沒法保證其中的共享資源仍舊是正確的

  優勢:使用簡單;保證鎖確定會被釋放;速度同 Moniter 。

  缺點:同 Moniter。

 

 

  ReadWriteLockSlim 與 Mointer 不一樣,它經過 EnterReadLock、EnterWriteLock、ExitReadLock、ExitWriteLock 來區別對待讀線程仍是寫線程。因此對於讀線程加讀鎖,而寫線程加寫鎖,這樣噹噹前時刻不存在寫線程的時候,全部讀線程均可以併發的訪問資源。

  優勢:讀、寫鎖分離。當不存在寫線程的時候,速度要明顯快於 Mointer。而當有寫線程的時候,速度稍慢於 Mointer。

 

  上面的方式各有優缺點,就算是經驗豐富的程序猿也不必定能保證線程必定是安全的。因此只要有可能仍是建議你們儘可能不使用、少使用共享資源,或者讓共享資源變成只讀

 

總結

  情景一中所說的全部方法都是圍繞一個目的 ------ 「解決對共享資源的爭用問題」。當在實際開發過程當中,若是碰到了共享資源(靜態變量、類型的成員變量、文件等)或須要獨佔使用的代碼段時,請考慮採用上述方式中的任何一種來保證線程安全。

 

  本文來自《C# 基礎回顧: 線程同步的情景之一

相關文章
相關標籤/搜索