C#線程鎖使用全功略


前兩篇簡單介紹了線程同步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()的例子,該方法以原子的形式遞增指定變量並存儲結果,示例如下:


    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略有不同)的源碼如下:


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提到:從頭到尾對一個集合進行枚舉本質上並不是一個線程安全的過程。即使一個集合已進行同步,其他線程仍可以修改該集合,這將 導致枚舉數引發異常。若要在枚舉過程中保證線程安全,可以在整個枚舉過程中鎖定集合,或者捕捉由於其他線程進行的更改而引發的異常。應該使用下面的代碼:

Queue使用lock示例

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


    //在多線程環境中只要我們用下面的 方式實例化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()
    {
        lock (this._table.SyncRoot)
        {
            this._table.Clear();
        }
    }

    public override void Remove(object key)
    {
        lock (this._table.SyncRoot)
        {
            this._table.Remove(key);
        }
    }

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM