程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C#編程總結(三)線程同步

C#編程總結(三)線程同步

編輯:關於C#

在應用程序中使用多個線程的一個好處是每個線程都可以異步執行。對於 Windows 應用程序,耗時 的任務可以在後台執行,而使應用程序窗口和控件保持響應。對於服務器應用程序,多線程處理提供了 用不同線程處理每個傳入請求的能力。否則,在完全滿足前一個請求之前,將無法處理每個新請求。然 而,線程的異步特性意味著必須協調對資源(如文件句柄、網絡連接和內存)的訪問。否則,兩個或更 多的線程可能在同一時間訪問相同的資源,而每個線程都不知道其他線程的操作。

"如果覺得有用,請幫頂! 如果有不足之處,歡迎拍磚!"

線程同步的方式

線程同步有:臨界區、互斥區、事件、信號量四種方式

臨界區(Critical Section)、互斥量 (Mutex)、信號量(Semaphore)、事件(Event)的區別

1、臨界區:通過對多線程的串行化來訪 問公共資源或一段代碼,速度快,適合控制數據訪問。在任意時刻只允許一個線程對共享資源進行訪問 ,如果有多個線程試圖訪問公共資源,那麼在有一個線程進入後,其他試圖訪問公共資源的線程將被掛 起,並一直等到進入臨界區的線程離開,臨界區在被釋放後,其他線程才可以搶占。

2、互斥量:采 用互斥對象機制。 只有擁有互斥對象的線程才有訪問公共資源的權限,因為互斥對象只有一個,所以能 保證公共資源不會同時被多個線程訪問。互斥不僅能實現同一應用程序的公共資源安全共享,還能實現 不同應用程序的公共資源安全共享

3、信號量:它允許多個線程在同一時刻訪問同一資源,但是需要 限制在同一時刻訪問此資源的最大線程數目

4、事 件: 通過通知操作的方式來保持線程的同步,還 可以方便實現對多個線程的優先級比較的操作

C#中常見線程同步方法

我們介紹幾種常用的C#進行線程同步的方式,這些方式可以根據其原理,找到對應上面的四種類型之 一。

1、Interlocked
為多個線程共享的變量提供原子操作。

根據經驗,那些需要在多線程情況下被保護的資源通常是整型值,且這些整型值在多線程下最常見的 操作就是遞增、遞減或相加操作。Interlocked類提供了一個專門的機制用於完成這些特定的操作。這個 類提供了Increment、Decrement、Add靜態方法用於對int或long型變量的遞增、遞減或相加操作。此類 的方法可以防止可能在下列情況發生的錯誤:計劃程序在某個線程正在更新可由其他線程訪問的變量時 切換上下文;或者當兩個線程在不同的處理器上並發執行時。 此類的成員不引發異常。

Increment 和 Decrement 方法遞增或遞減變量並將結果值存儲在單個操作中。 在大多數計算機上, 增加變量操作不是一個原子操作,需要執行下列步驟:

1)將實例變量中的值加載到寄存器中。
2)增加或減少該值。
3)在實例變量中存儲該值。
如果不使用 Increment 和 Decrement,線程會在執行完前兩個步驟後被搶先。 然後由另一個線程執行 所有三個步驟。 當第一個線程重新開始執行時,它覆蓋實例變量中的值,造成第二個線程執行增減操作 的結果丟失。

Exchange 方法自動交換指定變量的值。 CompareExchange 方法組合了兩個操作:比較兩個值以及根 據比較的結果將第三個值存儲在其中一個變量中。 比較和交換操作按原子操作執行。

案例分析:共享打印機。

通常我們會使用共享打印機,幾台計算機共享一台打印機,每台計算機可以發出打印指令,可能會出 現並發情況。當然我們知道,打印機采用了隊列技術。為了簡化操作,我們假定,在打印機收到命令時 ,即可打印,而且在同一時間只能有一個打印任務在執行。我們使用Interlocked方法來實現多線程同步 。具體代碼如下:

using System;
using System.Threading;
   
namespace MutiThreadSample.ThreadSynchronization
{
    class PrinterWithInterlockTest
    {
        /// <summary>
        /// 正在使用的打印機
        /// 0代表未使用,1代表正在使用
        /// </summary>
        public static int UsingPrinter = 0;
        /// <summary>
        /// 計算機數量
        /// </summary>
        public static readonly int ComputerCount = 3;
        /// <summary>
        /// 測試
        /// </summary>
        public static void TestPrint()
        {
            Thread thread;
            Random random = new Random();
            for (int i = 0; i < ComputerCount; i++)
            {
                thread = new Thread(MyThreadProc);
                thread.Name = string.Format("Thread{0}",i);
                Thread.Sleep(random.Next(3));
                thread.Start();
            }
        }
        /// <summary>
        /// 線程執行操作
        /// </summary>
        private static void MyThreadProc()
        {
            //使用打印機進行打印
            UsePrinter();
            //當前線程等待1秒
            Thread.Sleep(1000);
        }
        /// <summary>
        /// 使用打印機進行打印
        /// </summary>
        private static bool UsePrinter()
        {
            //檢查大引進是否在使用,如果原始值為0,則為未使用,可以進行打印,否則不能打印,繼續等待
            if (0 == Interlocked.Exchange(ref UsingPrinter, 1))
            {
                Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name);
   
                //Code to access a resource that is not thread safe would go here.
   
                //Simulate some work
                Thread.Sleep(500);
   
                Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name);
   
                //釋放打印機
                Interlocked.Exchange(ref UsingPrinter, 0);
                return true;
            }
            else
            {
                Console.WriteLine("   {0} was denied the lock", Thread.CurrentThread.Name);
                return false;
            }
        }
   
    }
}

2、lock 關鍵字

lock 關鍵字將語句塊標記為臨界區,方法是獲取給定對象的互斥鎖,執行語句,然後釋放該鎖。
lock 確保當一個線程位於代碼的臨界區時,另一個線程不進入臨界區。如果其他線程試圖進入鎖定的代 碼,則它將一直等待(即被阻止),直到該對象被釋放。

public void Function()
{
      System.Object locker= new System.Object();
      lock(locker)
     {
           // Access thread-sensitive resources.
     }
}

lock 調用塊開始位置的 Enter 和塊結束位置的 Exit。

提供給 lock 關鍵字的參數必須為基於引用類型的對象,該對象用來定義鎖的范圍。在上例中,鎖的 范圍限定為此函數,因為函數外不存在任何對該對象的引用。嚴格地說,提供給 lock 的對象只是用來 唯一地標識由多個線程共享的資源,所以它可以是任意類實例。然而,實際上,此對象通常表示需要進 行線程同步的資源。例如,如果一個容器對象將被多個線程使用,則可以將該容器傳遞給 lock,而 lock 後面的同步代碼塊將訪問該容器。只要其他線程在訪問該容器前先鎖定該容器,則對該對象的訪問 將是安全同步的。通常,最好避免鎖定 public 類型或鎖定不受應用程序控制的對象實例,例如,如果 該實例可以被公開訪問,則 lock(this) 可能會有問題,因為不受控制的代碼也可能會鎖定該對象。這 可能導致死鎖,即兩個或更多個線程等待釋放同一對象。出於同樣的原因,鎖定公共數據類型(相比於 對象)也可能導致問題。鎖定字符串尤其危險,因為字符串被公共語言運行庫 (CLR)“暫留 ”。這意味著整個程序中任何給定字符串都只有一個實例,就是這同一個對象表示了所有運行的應 用程序域的所有線程中的該文本。因此,只要在應用程序進程中的任何位置處具有相同內容的字符串上 放置了鎖,就將鎖定應用程序中該字符串的所有實例。因此,最好鎖定不會被暫留的私有或受保護成員 。某些類提供專門用於鎖定的成員。例如,Array 類型提供 SyncRoot。許多集合類型也提供 SyncRoot 。

常見的結構 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 違反此准則:

1)如果實例可以被公共訪問,將出現 lock (this) 問題。
2)如果 MyType 可以被公共訪問,將出現 lock (typeof (MyType)) 問題。
3)由於進程中使用同一字符串的任何其他代碼將共享同一個鎖,所以出現 lock (“myLock”) 問題。
最佳做法是定義 private 對象來鎖定, 或 private static 對象變量來保護所有實例所共有的數據。關 於鎖的研究,大家可以參考:

http://www.cnblogs.com/yank/archive/2008/10/28/1321119.html

案例分析:繼續使用共享打印機的案例

我們只需對前面的例子稍作修改即可實現lock進行同步。

聲明鎖對象:

 /// <summary>

   /// 正在使用的打印機

   /// </summary>

   private static object UsingPrinterLocker = new object ();

將打印方法修改如下:

/// <summary>
        /// 使用打印機進行打印
        /// </summary>
        private static void UsePrinter()
        {
            //臨界區
            lock (UsingPrinterLocker)
            {
                Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name);
                //模擬打印操作
                Thread.Sleep(500);
                Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name);
            }
        }

3、監視器

與 lock 關鍵字類似,監視器防止多個線程同時執行代碼塊。Enter 方法允許一個且僅一個線程繼續 執行後面的語句;其他所有線程都將被阻止,直到執行語句的線程調用 Exit。這與使用 lock 關鍵字一 樣。事實上,lock 關鍵字就是用 Monitor 類來實現的。例如:(繼續修改共享打印機案例,增加方法 UsePrinterWithMonitor)

/// <summary>
        /// 使用打印機進行打印
        /// </summary>
        private static void UsePrinterWithMonitor()
        {
            System.Threading.Monitor.Enter(UsingPrinterLocker);
            try
            {
                Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name);
                //模擬打印操作
                Thread.Sleep(500);
                Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name);
            }
            finally
            {
                System.Threading.Monitor.Exit(UsingPrinterLocker);
            }
        }

使用 lock 關鍵字通常比直接使用 Monitor 類更可取,一方面是因為 lock 更簡潔,另一方面是因 為 lock 確保了即使受保護的代碼引發異常,也可以釋放基礎監視器。這是通過 finally 關鍵字來實現 的,無論是否引發異常它都執行關聯的代碼塊。

4、同步事件和等待句柄

使用鎖或監視器對於防止同時執行區分線程的代碼塊很有用,但是這些構造不允許一個線程向另一個 線程傳達事件。這需要“同步事件”,它是有兩個狀態(終止和非終止)的對象,可以用來 激活和掛起線程。讓線程等待非終止的同步事件可以將線程掛起,將事件狀態更改為終止可以將線程激 活。如果線程試圖等待已經終止的事件,則線程將繼續執行,而不會延遲。    

同步事件有兩種:AutoResetEvent 和 ManualResetEvent。它們之間唯一的不同在於,無論何時,只 要 AutoResetEvent 激活線程,它的狀態將自動從終止變為非終止。相反,ManualResetEvent 允許它的 終止狀態激活任意多個線程,只有當它的 Reset 方法被調用時才還原到非終止狀態。

等待句柄,可以通過調用一種等待方法,如 WaitOne、WaitAny 或 WaitAll,讓線程等待事件。 System.Threading.WaitHandle.WaitOne 使線程一直等待,直到單個事件變為終止狀態; System.Threading.WaitHandle.WaitAny 阻止線程,直到一個或多個指示的事件變為終止狀態; System.Threading.WaitHandle.WaitAll 阻止線程,直到所有指示的事件都變為終止狀態。當調用事件 的 Set 方法時,事件將變為終止狀態。

AutoResetEvent 允許線程通過發信號互相通信。 通常,當線程需要獨占訪問資源時使用該類。線程 通過調用 AutoResetEvent 上的 WaitOne 來等待信號。 如果 AutoResetEvent 為非終止狀態,則線程 會被阻止,並等待當前控制資源的線程通過調用 Set 來通知資源可用。調用 Set 向 AutoResetEvent 發信號以釋放等待線程。 AutoResetEvent 將保持終止狀態,直到一個正在等待的線程被釋放,然後自 動返回非終止狀態。 如果沒有任何線程在等待,則狀態將無限期地保持為終止狀態。如果當 AutoResetEvent 為終止狀態時線程調用 WaitOne,則線程不會被阻止。 AutoResetEvent 將立即釋放線 程並返回到非終止狀態。
可以通過將一個布爾值傳遞給構造函數來控制 AutoResetEvent 的初始狀態:如果初始狀態為終止狀態 ,則為 true;否則為 false。
AutoResetEvent 也可以同 staticWaitAll 和 WaitAny 方法一起使用。
案例:

案例介紹:

今天我們來做飯,做飯呢,需要一菜、一粥。今天我們吃魚。

熬粥和做魚,是比較復雜的工作流程,
做粥:選材、淘米、熬制
做魚:洗魚、切魚、腌制、烹調
為了提高效率,我們用兩個線程來准備這頓飯,但是,現在只有一口鍋,只能等一個做完之後,另一個 才能進行最後的烹調。

來看實例代碼:

using System;
using System.Threading;
   
namespace MutiThreadSample.ThreadSynchronization
{
    /// <summary>
    /// 案例:做飯
    /// 今天的Dinner准備吃魚,還要熬粥
    /// 熬粥和做魚,是比較復雜的工作流程,
    /// 做粥:選材、淘米、熬制
    /// 做魚:洗魚、切魚、腌制、烹調
    /// 我們用兩個線程來准備這頓飯
    /// 但是,現在只有一口鍋,只能等一個做完之後,另一個才能進行最後的烹調
    /// </summary>
    class CookResetEvent
    {
        /// <summary>
        /// 
        /// </summary>
        private AutoResetEvent resetEvent = new AutoResetEvent(false);
        /// <summary>
        /// 做飯
        /// </summary>
        public void Cook()
        {
            Thread porridgeThread = new Thread(new ThreadStart(Porridge));
            porridgeThread.Name = "Porridge";
            porridgeThread.Start();
   
            Thread makeFishThread = new Thread(new ThreadStart(MakeFish));
            makeFishThread.Name = "MakeFish";
            makeFishThread.Start();
   
            //等待5秒
            Thread.Sleep(5000);
   
            resetEvent.Reset();
        }
        /// <summary>
        /// 熬粥
        /// </summary>
        public void Porridge()
        { 
            //選材
            Console.WriteLine("Thread:{0},開始選材", Thread.CurrentThread.Name);
   
            //淘米
            Console.WriteLine("Thread:{0},開始淘米", Thread.CurrentThread.Name);
   
            //熬制
            Console.WriteLine("Thread:{0},開始熬制,需要2秒鐘", Thread.CurrentThread.Name);
            //需要2秒鐘
            Thread.Sleep(2000);
            Console.WriteLine("Thread:{0},粥已經做好,鍋閒了", Thread.CurrentThread.Name);
   
            resetEvent.Set();
        }
        /// <summary>
        /// 做魚
        /// </summary>
        public void MakeFish()
        { 
            //洗魚
            Console.WriteLine("Thread:{0},開始洗魚",Thread.CurrentThread.Name);
   
            //腌制
            Console.WriteLine("Thread:{0},開始腌制", Thread.CurrentThread.Name);
   
            //等待鍋空閒出來
            resetEvent.WaitOne();
   
            //烹調
            Console.WriteLine("Thread:{0},終於有鍋了", Thread.CurrentThread.Name);
            Console.WriteLine("Thread:{0},開始做魚,需要5秒鐘", Thread.CurrentThread.Name);
            Thread.Sleep(5000);
            Console.WriteLine("Thread:{0},魚做好了,好香", Thread.CurrentThread.Name);
   
            resetEvent.Set();
        }
    }
}

ManualResetEvent與AutoResetEvent用法基本類似,這裡不多做介紹。

5、Mutex對象

mutex 與監視器類似;它防止多個線程在某一時間同時執行某個代碼塊。事實上,名稱 “mutex”是術語“互相排斥 (mutually exclusive)”的簡寫形式。然而與監視 器不同的是,mutex 可以用來使跨進程的線程同步。mutex 由 Mutex 類表示。當用於進程間同步時, mutex 稱為“命名 mutex”,因為它將用於另一個應用程序,因此它不能通過全局變量或靜 態變量共享。必須給它指定一個名稱,才能使兩個應用程序訪問同一個 mutex 對象。

 盡管 mutex 可以用於進程內的線程同步,但是使用 Monitor 通常更為可取,因為監視器是專門為 .NET Framework 而設計的,因而它可以更好地利用資源。相比之下,Mutex 類是 Win32 構造的包裝。盡管 mutex 比監視器更為強大,但是相對於 Monitor 類,它所需要的互操作轉換更消耗計算資源。

本地 mutex 和系統 mutex

 Mutex 分兩種類型:本地 mutex 和命名系統 mutex。 如果使 用接受名稱的構造函數創建了 Mutex 對象,那麼該對象將與具有該名稱的操作系統對象相關聯。 命名 的系統 mutex 在整個操作系統中都可見,並且可用於同步進程活動。 您可以創建多個 Mutex 對象來表 示同一命名系統 mutex,而且您可以使用 OpenExisting 方法打開現有的命名系統 mutex。

  本地 mutex 僅存在於進程當中。 進程中引用本地 Mutex 對象的任意線程都可以使用本地 mutex。 每 個 Mutex 對象都是一個單獨的本地 mutex。

在本地Mutex中,用法與Monitor基本一致

繼續修改前面的打印機案例:

聲明Mutex對象:

 /// <summary>

   /// mutex對象

   /// </summary>

   private static Mutex mutex = new Mutex();

具體操作:

/// <summary>
        /// 使用打印機進行打印
        /// </summary>
        private static void UsePrinterWithMutex()
        {
            mutex.WaitOne();
            try
            {
                Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name);
                //模擬打印操作
                Thread.Sleep(500);
                Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name);
            }
            finally
            {
                mutex.ReleaseMutex();
            }
        }

多線程調用:

/// <summary>
        /// 測試
        /// </summary>
        public static void TestPrint()
        {
            Thread thread;
            Random random = new Random();
            for (int i = 0; i < ComputerCount; i++)
            {
                thread = new Thread(MyThreadProc);
                thread.Name = string.Format("Thread{0}", i);
                Thread.Sleep(random.Next(3));
                thread.Start();
            }
        }
        /// <summary>
        /// 線程執行操作
        /// </summary>
        private static void MyThreadProc()
        {
            //使用打印機進行打印
            //UsePrinter();
            //monitor同步
            //UsePrinterWithMonitor();
            //用Mutex同步
            UsePrinterWithMutex();
            //當前線程等待1秒
            Thread.Sleep(1000);
        }

最後的打印機案例代碼:

using System;
using System.Threading;
   
namespace MutiThreadSample.ThreadSynchronization
{
    class PrinterWithLockTest
    {
        /// <summary>
        /// 正在使用的打印機
        /// </summary>
        private static object UsingPrinterLocker = new object();
        /// <summary>
        /// 計算機數量
        /// </summary>
        public static readonly int ComputerCount = 3;
        /// <summary>
        /// mutex對象
        /// </summary>
        private static Mutex mutex = new Mutex();
        /// <summary>
        /// 測試
        /// </summary>
        public static void TestPrint()
        {
            Thread thread;
            Random random = new Random();
            for (int i = 0; i < ComputerCount; i++)
            {
                thread = new Thread(MyThreadProc);
                thread.Name = string.Format("Thread{0}", i);
                Thread.Sleep(random.Next(3));
                thread.Start();
            }
        }
        /// <summary>
        /// 線程執行操作
        /// </summary>
        private static void MyThreadProc()
        {
            //使用打印機進行打印
            //UsePrinter();
            //monitor同步
            //UsePrinterWithMonitor();
            //用Mutex同步
            UsePrinterWithMutex();
            //當前線程等待1秒
            Thread.Sleep(1000);
        }
        /// <summary>
        /// 使用打印機進行打印
        /// </summary>
        private static void UsePrinter()
        {
            //臨界區
            lock (UsingPrinterLocker)
            {
                Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name);
                //模擬打印操作
                Thread.Sleep(500);
                Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name);
            }
        }
   
        /// <summary>
        /// 使用打印機進行打印
        /// </summary>
        private static void UsePrinterWithMonitor()
        {
            System.Threading.Monitor.Enter(UsingPrinterLocker);
            try
            {
                Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name);
                //模擬打印操作
                Thread.Sleep(500);
                Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name);
            }
            finally
            {
                System.Threading.Monitor.Exit(UsingPrinterLocker);
            }
        }
   
        /// <summary>
        /// 使用打印機進行打印
        /// </summary>
        private static void UsePrinterWithMutex()
        {
            mutex.WaitOne();
            try
            {
                Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name);
                //模擬打印操作
                Thread.Sleep(500);
                Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name);
            }
            finally
            {
                mutex.ReleaseMutex();
            }
        }
    }
}

6、讀取器/編寫器鎖

ReaderWriterLockSlim 類允許多個線程同時讀取一個資源,但在向該資源寫入時要求線程等待以獲 得獨占鎖。

可以在應用程序中使用 ReaderWriterLockSlim,以便在訪問一個共享資源的線程之間提供協調同步 。 獲得的鎖是針對 ReaderWriterLockSlim 本身的。

 設計您應用程序的結構,讓讀取和寫入 操作的時間盡可能最短。 因為寫入鎖是排他的,所以長時間的寫入操作會直接影響吞吐量。 長時間的 讀取操作會阻止處於等待狀態的編寫器,並且,如果至少有一個線程在等待寫入訪問,則請求讀取訪問 的線程也將被阻止。

案例:構造一個線程安全的緩存

using System;
using System.Threading;
using System.Collections.Generic;
   
   
namespace MutiThreadSample.ThreadSynchronization
{
    /// <summary>
    /// 同步Cache
    /// </summary>
    public class SynchronizedCache
    {
        private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
        private Dictionary<int, string> innerCache = new Dictionary<int, string>();
        /// <summary>
        /// 讀取
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public string Read(int key)
        {
            cacheLock.EnterReadLock();
            try
            {
                return innerCache[key];
            }
            finally
            {
                cacheLock.ExitReadLock();
            }
        }
        /// <summary>
        /// 添加項
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void Add(int key, string value)
        {
            cacheLock.EnterWriteLock();
            try
            {
                innerCache.Add(key, value);
            }
            finally
            {
                cacheLock.ExitWriteLock();
            }
        }
        /// <summary>
        /// 添加項,有超時限制
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="timeout"></param>
        /// <returns></returns>
        public bool AddWithTimeout(int key, string value, int timeout)
        {
            if (cacheLock.TryEnterWriteLock(timeout))
            {
                try
                {
                    innerCache.Add(key, value);
                }
                finally
                {
                    cacheLock.ExitWriteLock();
                }
                return true;
            }
            else
            {
                return false;
            }
        }
        /// <summary>
        /// 添加或者更新
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public AddOrUpdateStatus AddOrUpdate(int key, string value)
        {
            cacheLock.EnterUpgradeableReadLock();
            try
            {
                string result = null;
                if (innerCache.TryGetValue(key, out result))
                {
                    if (result == value)
                    {
                        return AddOrUpdateStatus.Unchanged;
                    }
                    else
                    {
                        cacheLock.EnterWriteLock();
                        try
                        {
                            innerCache[key] = value;
                        }
                        finally
                        {

                            cacheLock.ExitWriteLock();
                        }
                        return AddOrUpdateStatus.Updated;
                    }
                }
                else
                {
                    cacheLock.EnterWriteLock();
                    try
                    {
                        innerCache.Add(key, value);
                    }
                    finally
                    {
                        cacheLock.ExitWriteLock();
                    }
                    return AddOrUpdateStatus.Added;
                }
            }
            finally
            {
                cacheLock.ExitUpgradeableReadLock();
            }
        }
        /// <summary>
        /// 刪除項
        /// </summary>
        /// <param name="key"></param>
        public void Delete(int key)
        {
            cacheLock.EnterWriteLock();
            try
            {
                innerCache.Remove(key);
            }
            finally
            {
                cacheLock.ExitWriteLock();
            }
        }
        /// <summary>
        /// 
        /// </summary>
        public enum AddOrUpdateStatus
        {
            Added,
            Updated,
            Unchanged
        };
    }
}

7、Semaphore 和 SemaphoreSlim

System.Threading.Semaphore 類表示一個命名(系統范圍)信號量或本地信號量。 它是一個對 Win32 信號量對象的精簡包裝。 Win32 信號量是計數信號量,可用於控制對資源池的訪問。

 SemaphoreSlim 類表示一個輕量的快速信號量,可用於在一個預計等待時間會非常短的進程內進 行等待。 SemaphoreSlim 會盡可能多地依賴由公共語言運行時 (CLR) 提供的同步基元。 但是,它也會 根據需要提供延遲初始化的、基於內核的等待句柄,以支持等待多個信號量。 SemaphoreSlim 還支持使 用取消標記,但它不支持命名信號量或使用等待句柄來進行同步。

線程通過調用 WaitOne 方法來進入信號量,此方法是從 WaitHandle 類派生的。 當調用返回時,信 號量的計數將減少。 當一個線程請求項而計數為零時,該線程會被阻止。 當線程通過調用 Release 方 法釋放信號量時,將允許被阻止的線程進入。 並不保證被阻塞的線程進入信號量的順序,例如先進先出 (FIFO) 或後進先出 (LIFO)。信號量的計數在每次線程進入信號量時減小,在線程釋放信號量時增加。 當計數為零時,後面的請求將被阻塞,直到有其他線程釋放信號量。 當所有的線程都已釋放信號量時, 計數達到創建信號量時所指定的最大值。

案例分析:購買火車票

還得排隊進行購買,購買窗口是有限的,只有窗口空閒時才能購買

using System;
using System.Threading;
   
namespace MutiThreadSample.ThreadSynchronization
{
    /// <summary>
    /// 案例:支付流程
    /// 如超市、藥店、火車票等,都有限定的幾個窗口進行結算,只有有窗口空閒,才能進行結算。
    /// 我們就用多線程來模擬結算過程
    /// </summary>
    class PaymentWithSemaphore
    {
        /// <summary>
        /// 聲明收銀員總數為3個,但是當前空閒的個數為0,可能還沒開始上班。
        /// </summary>
        private static Semaphore IdleCashiers = new Semaphore(0, 3);
        /// <summary>
        /// 測試支付過程
        /// </summary>
        public static void TestPay()
        {
            ParameterizedThreadStart start = new ParameterizedThreadStart(Pay);
            //假設同時有5個人來買票
            for (int i = 0; i < 5; i++)
            {
                Thread thread = new Thread(start);
                thread.Start(i);
            }
   
            //主線程等待,讓所有的的線程都激活
            Thread.Sleep(1000);
            //釋放信號量,2個收銀員開始上班了或者有兩個空閒出來了
            IdleCashiers.Release(2);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="obj"></param>
        public static void Pay(object obj)
        {
            Console.WriteLine("Thread {0} begins and waits for the semaphore.", obj);
            IdleCashiers.WaitOne();
            Console.WriteLine("Thread {0} starts to Pay.",obj);
            //結算
            Thread.Sleep(2000);
            Console.WriteLine("Thread {0}: The payment has been finished.",obj);
   
            Console.WriteLine("Thread {0}: Release the semaphore.", obj);
            IdleCashiers.Release();
        }
    }
}
   
購買火車票

8、障礙(Barrier)4.0後技術

使多個任務能夠采用並行方式依據某種算法在多個階段中協同工作。
通過在一系列階段間移動來協作完成一組任務,此時該組中的每個任務發信號指出它已經到達指定階段 的 Barrier 並且暗中等待其他任務到達。 相同的 Barrier 可用於多個階段。

9、SpinLock(4.0後)

SpinLock結構是一個低級別的互斥同步基元,它在等待獲取鎖時進行旋轉。 在多核計算機上,當等待時間預計較短且極少出現爭用情況時,SpinLock 的性能將高於其他類型的鎖。 不過,我們建議您僅在通過分析確定 System.Threading.Monitor 方法或 Interlocked 方法顯著降低了 程序的性能時使用 SpinLock。

 即使 SpinLock 未獲取鎖,它也會產生線程的時間片。 它這 樣做是為了避免線程優先級別反轉,並使垃圾回收器能夠繼續執行。 在使用 SpinLock 時,請確保任何 線程持有鎖的時間不會超過一個非常短的時間段,並確保任何線程在持有鎖時不會阻塞。

 由 於 SpinLock 是一個值類型,因此,如果您希望兩個副本都引用同一個鎖,則必須通過引用顯式傳遞該 鎖。

using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
   
namespace MutiThreadSample.ThreadSynchronization
{
    class SpinLockSample
    {
        public static void Test()
        {
            SpinLock sLock = new SpinLock();
            StringBuilder sb = new StringBuilder();
            Action action = () =>
            {
                bool gotLock = false;
                for (int i = 0; i < 100; i++)
                {
                    gotLock = false;
                    try
                    {
                        sLock.Enter(ref gotLock);
                        sb.Append(i.ToString());
                    }
                    finally
                    {
                        //真正獲取之後,才釋放
                        if (gotLock) sLock.Exit();
                    }
                }
            };
   
            //多線程調用action
            Parallel.Invoke(action, action, action);
            Console.WriteLine("輸出:{0}",sb.ToString());
        }
    }
}

10、SpinWait(4.0後)

System.Threading.SpinWait 是一個輕量同步類型,可以在低級別方案中使用它來避免內核事件所需 的高開銷的上下文切換和內核轉換。 在多核計算機上,當預計資源不會保留很長一段時間時,如果讓等 待線程以用戶模式旋轉數十或數百個周期,然後重新嘗試獲取資源,則效率會更高。 如果在旋轉後資源 變為可用的,則可以節省數千個周期。 如果資源仍然不可用,則只花費了少量周期,並且仍然可以進行 基於內核的等待。 這一旋轉-等待的組合有時稱為“兩階段等待操作”。

下面的基本示例采用微軟案例:無鎖堆棧

using System;
using System.Threading;
   
namespace MutiThreadSample.ThreadSynchronization
{
    public class LockFreeStack<T>
    {
        private volatile Node m_head;
   
        private class Node { public Node Next; public T Value; }
   
        public void Push(T item)
        {
            var spin = new SpinWait();
            Node node = new Node { Value = item }, head;
            while (true)
            {
                head = m_head;
                node.Next = head;
                if (Interlocked.CompareExchange(ref m_head, node, head) == head) break;
                spin.SpinOnce();
            }
        }
   
        public bool TryPop(out T result)
        {
            result = default(T);
            var spin = new SpinWait();
   
            Node head;
            while (true)
            {
                head = m_head;
                if (head == null) return false;
                if (Interlocked.CompareExchange(ref m_head, head.Next, head) == head)
                {
                    result = head.Value;
                    return true;
                }
                spin.SpinOnce();
            }
        }
    }
}

總結:

盡管有這麼多的技術,但是不同的技術對應不同的場景,我們必須熟悉其特點和適用范圍。在應用時 ,必須具體問題具體分析,選擇最佳的同步方式。

查看本欄目

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved