天天看點

多線程:C#線程同步lock,Monitor,Mutex,同步事件和等待句柄(下)

轉自 http://www.cnblogs.com/freshman0216/archive/2008/08/07/1256919.html

   前兩篇簡單介紹了線程同步lock,Monitor,同步事件EventWaitHandler,互斥體Mutex的基本用法,在此基礎上,我們對它們用法進行比較,并給出什麼時候需要鎖什麼時候不需要的幾點建議。最後,介紹幾個FCL中線程安全的類,集合類的鎖定方式等,做為對線程同步系列的完善和補充。

      1.幾種同步方法的差別

      lock和Monitor是.NET用一個特殊結構實作的,Monitor對象是完全托管的、完全可移植的,并且在作業系統資源要求方面可能更為有效,同步速度較快,但不能跨程序同步。lock(Monitor.Enter和Monitor.Exit方法的封裝),主要作用是鎖定臨界區,使臨界區代碼隻能被獲得鎖的線程執行。Monitor.Wait和Monitor.Pulse用于線程同步,類似信号操作,個人感覺使用比較複雜,容易造成死鎖。

      互斥體Mutex和事件對象EventWaitHandler屬于核心對象,利用核心對象進行線程同步,線程必須要在使用者模式和核心模式間切換,是以一般效率很低,但利用互斥對象和事件對象這樣的核心對象,可以在多個程序中的各個線程間進行同步。

      互斥體Mutex類似于一個接力棒,拿到接力棒的線程才可以開始跑,當然接力棒一次隻屬于一個線程(Thread Affinity),如果這個線程不釋放接力棒(Mutex.ReleaseMutex),那麼沒辦法,其他所有需要接力棒運作的線程都知道能等着看熱鬧。

      EventWaitHandle 類允許線程通過發信号互相通信。通常,一個或多個線程在 EventWaitHandle 上阻止,直到一個未阻止的線程調用 Set 方法,以釋放一個或多個被阻止的線程。

      2.什麼時候需要鎖定

      首先要了解鎖定是解決競争條件的,也就是多個線程同時通路某個資源,造成意想不到的結果。比如,最簡單的情況是,一個計數器,兩個線程 同時加一,後果就是損失了一個計數,但相當頻繁的鎖定又可能帶來性能上的消耗,還有最可怕的情況死鎖。那麼什麼情況下我們需要使用鎖,什麼情況下不需要 呢?

      1)隻有共享資源才需要鎖定

      隻有可以被多線程通路的共享資源才需要考慮鎖定,比如靜态變量,再比如某些緩存中的值,而屬于線程内部的變量不需要鎖定。 

      2)多使用lock,少用Mutex

      如果你一定要使用鎖定,請盡量不要使用核心子產品的鎖定機制,比如.NET的Mutex,Semaphore,AutoResetEvent和 ManuResetEvent,使用這樣的機制涉及到了系統在使用者模式和核心模式間的切換,性能差很多,但是他們的優點是可以跨程序同步線程,是以應該清 楚的了解到他們的不同和适用範圍。

      3)了解你的程式是怎麼運作的

      實際上在web開發中大多數邏輯都是在單個線程中展開的,一個請求都會在一個單獨的線程中處理,其中的大部分變量都是屬于這個線程的,根本沒有必要考慮鎖定,當然對于ASP.NET中的Application對象中的資料,我們就要考慮加鎖了。

      4)把鎖定交給資料庫

      數 據庫除了存儲資料之外,還有一個重要的用途就是同步,資料庫本身用了一套複雜的機制來保證資料的可靠和一緻性,這就為我們節省了很多的精力。保證了資料源 頭上的同步,我們多數的精力就可以集中在緩存等其他一些資源的同步通路上了。通常,隻有涉及到多個線程修改資料庫中同一條記錄時,我們才考慮加鎖。 

      5)業務邏輯對事務和線程安全的要求

      這 條是最根本的東西,開發完全線程安全的程式是件很費時費力的事情,在電子商務等涉及金融系統的案例中,許多邏輯都必須嚴格的線程安全,是以我們不得不犧牲 一些性能,和很多的開發時間來做這方面的工作。而一般的應用中,許多情況下雖然程式有競争的危險,我們還是可以不使用鎖定,比如有的時候計數器少一多一, 對結果無傷大雅的情況下,我們就可以不用去管它。

      3.InterLocked類

      Interlocked 類提供了同步對多個線程共享的變量的通路的方法。如果該變量位于共享記憶體中,則不同程序的線程就可以使用該機制。互鎖操作是原子的,即整個操作是不能由相 同變量上的另一個互鎖操作所中斷的單元。這在搶先多線程作業系統中是很重要的,在這樣的作業系統中,線程可以在從某個記憶體位址加載值之後但是在有機會更改 和存儲該值之前被挂起。

      我們來看一個InterLock.Increment()的例子,該方法以原子的形式遞增指定變量并存儲結果,示例如下:

多線程:C#線程同步lock,Monitor,Mutex,同步事件和等待句柄(下)
多線程:C#線程同步lock,Monitor,Mutex,同步事件和等待句柄(下)

    class InterLockedTest

    {

        public static Int64 i = 0;

        public static void Add()

        {

            for (int i = 0; i < 100000000; i++)

            {

                Interlocked.Increment(ref InterLockedTest.i);

                //InterLockedTest.i = InterLockedTest.i + 1;

            }

        }

        public static void Main(string[] args)

            Thread t1 = new Thread(new ThreadStart(InterLockedTest.Add));

            Thread t2 = new Thread(new ThreadStart(InterLockedTest.Add));

            t1.Start();

            t2.Start();

            t1.Join();

            t2.Join();

            Console.WriteLine(InterLockedTest.i.ToString());

            Console.Read();

    }

      輸出結果200000000,如果InterLockedTest.Add()方法中用注釋掉的語句代替Interlocked.Increment()方法,結果将不可預知,每次執行結果不同。InterLockedTest.Add()方法保證了加1操作的原子性,功能上相當于自動給加操作使用了lock鎖。同時我們也注意到InterLockedTest.Add()用時比直接用+号加1要耗時的多,是以說加鎖資源損耗還是很明顯的。

      另外InterLockedTest類還有幾個常用方法,具體用法可以參考MSDN上的介紹。

      4.集合類的同步

      .NET在一些集合類,比如Queue、ArrayList、HashTable和Stack,已經提供了一個供lock使用的對象SyncRoot。用Reflector檢視了SyncRoot屬性(Stack.SynchRoot略有不同)的源碼如下:

多線程:C#線程同步lock,Monitor,Mutex,同步事件和等待句柄(下)
多線程:C#線程同步lock,Monitor,Mutex,同步事件和等待句柄(下)

public virtual object SyncRoot

{

    get

        if (this._syncRoot == null)

            //如果_syncRoot和null相等,将new object指派給_syncRoot

            //Interlocked.CompareExchange方法保證多個線程在使用syncRoot時是線程安全的

            Interlocked.CompareExchange(ref this._syncRoot, new object(), null);

        return this._syncRoot;

}

      這裡要特别注意的是MSDN提到:從頭到尾對一個集合進行枚舉本質上并不是一個線程安全的過程。即使一個集合已進行同步,其他線程仍可以修改該集合,這将導緻枚舉數引發異常。若要在枚舉過程中保證線程安全,可以在整個枚舉過程中鎖定集合,或者捕捉由于其他線程進行的更改而引發的異常。應該使用下面的代碼:

多線程:C#線程同步lock,Monitor,Mutex,同步事件和等待句柄(下)
多線程:C#線程同步lock,Monitor,Mutex,同步事件和等待句柄(下)

    Queue q = new Queue();

    lock (q.SyncRoot)

        foreach (object item in q)

            //do something

      還有一點需要說明的是,集合類提供了一個是和同步相關的方法Synchronized,該 方法傳回一個對應的集合類的wrapper類,該類是線程安全的,因為他的大部分方法都用lock關鍵字進行了同步處理。如HashTable的 Synchronized傳回一個新的線程安全的HashTable執行個體,代碼如下:

多線程:C#線程同步lock,Monitor,Mutex,同步事件和等待句柄(下)
多線程:C#線程同步lock,Monitor,Mutex,同步事件和等待句柄(下)

    //在多線程環境中隻要我們用下面的方式執行個體化HashTable就可以了

    Hashtable ht = Hashtable.Synchronized(new Hashtable());

    //以下代碼是.NET Framework Class Library實作,增加對Synchronized的認識

    [HostProtection(SecurityAction.LinkDemand, Synchronization=true)]

    public static Hashtable Synchronized(Hashtable table)

        if (table == null)

            throw new ArgumentNullException("table");

        return new SyncHashtable(table);

    //SyncHashtable的幾個常用方法,我們可以看到内部實作都加了lock關鍵字保證線程安全

    public override void Add(object key, object value)

        lock (this._table.SyncRoot)

            this._table.Add(key, value);

    public override void Clear()

            this._table.Clear();

    public override void Remove(object key)

            this._table.Remove(key);

      線程同步是一個非常複雜的話題,這裡隻是根據公司的一個項目把相關的知識整理出來,作為工作的一種總結。這些同步方法的使用場景是怎樣的?究竟有哪些細微的差别?還有待于進一步的學習和實踐。