1. 程式人生 > >C# 多線程並發鎖模式-總結

C# 多線程並發鎖模式-總結

了解 man 競爭 根據 建議 進一步 root 臨界區 遞歸

開篇:

互斥還是lock Monitor Mutex 模式!

Muex Monitor lock AutoEventSet ManualEventSet

後續的

ReaderWriterLock

ReaderWriterLockSlim 類

表示用於管理資源訪問的鎖定狀態,可實現多線程讀取或進行獨占式寫入訪問。

使用 ReaderWriterLockSlim 來保護由多個線程讀取但每次只采用一個線程寫入的資源。 ReaderWriterLockSlim 允許多個線程均處於讀取模式,允許一個線程處於寫入模式並獨占鎖定狀態,同時還允許一個具有讀取權限的線程處於可升級的讀取模式,在此模式下線程無需放棄對資源的讀取權限即可升級為寫入模式。

註意 ReaderWriterLockSlim 類似於 ReaderWriterLock,只是簡化了遞歸、升級和降級鎖定狀態的規則。 ReaderWriterLockSlim 可避免多種潛在的死鎖情況。 此外,ReaderWriterLockSlim 的性能明顯優於 ReaderWriterLock。 建議在所有新的開發工作中使用 ReaderWriterLockSlim。

以上引用自MSDN

C# 線程手冊 第三章 使用線程 ReaderWriterLock 類

2012-02-07 21:53 by DanielWise, 4166 閱讀, 1 評論, 收藏, 編輯

一個ReaderWriterLock 類定義一個實現單寫多讀語義的鎖。這個類通常用在能被多個線程讀取但是僅能被一個線程寫入的文件操作時使用。下面是ReaderWriterLock類中的四個主要方法:

a. AcquireReaderLock(): 這個重載方法獲取一個讀者鎖,接受一個整型或者TimeSpan類型的timeout 值。timeout是一個檢測死鎖的利器。

b. AcquireWriterLock(): 這個重載方法獲取一個寫者鎖,接受一個整型或者TimeSpan類型的timeout 值。

c. ReleaseReaderLock(): 釋放讀者鎖。

d. ReleaseWriterLock(): 釋放寫者鎖。

使用ReaderWriterLock類可以讓多線程安全地進行數據並發讀取。只有當線程正在更新的數據鎖定。讀者線程可以再沒有寫者擁有鎖的時候獲得鎖。寫者線程可以再沒有讀者線程或者寫者線程擁有鎖的時候獲得鎖。

下面的列表ReadeWriteLock.cs, 描述了如何使用ReaderWriterLock()鎖:

/*************************************
/* copyright (c) 2012 daniel dong
 * 
 * author:daniel dong
 * blog:  www.cnblogs.com/danielwise
 * email: [email protected]
 * 
 */

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace ReadWriteLock
{
    public class ReadWrite
    {
        private ReaderWriterLock rwl;
        private int x;
        private int y;

        public ReadWrite()
        {
            rwl = new ReaderWriterLock();
        }

        public void ReadInts(ref int a, ref int b)
        {
            rwl.AcquireReaderLock(Timeout.Infinite);
            try
            {
                a = this.x;
                b = this.y;
            }
            finally
            {
                rwl.ReleaseReaderLock();
            }
        }

        public void WriteInts(int a, int b)
        {
            rwl.AcquireWriterLock(Timeout.Infinite);
            try
            {
                this.x = a;
                this.y = b;
                Console.WriteLine("x = " + this.x
                    + " y = " + this.y
                    + " ThreadID = " + Thread.CurrentThread.GetHashCode());
            }
            finally
            {
                rwl.ReleaseWriterLock();
            }
        }
    }

    public class RWApp
    {
        private ReadWrite rw = new ReadWrite();

        public static void Main(string[] args)
        {
            RWApp e = new RWApp();

            //Writer Threads
            Thread wt1 = new Thread(new ThreadStart(e.Write));
            wt1.Start();
            Thread wt2 = new Thread(new ThreadStart(e.Write));
            wt2.Start();

            //Reader Threads
            Thread rt1 = new Thread(new ThreadStart(e.Read));
            rt1.Start();
            Thread rt2 = new Thread(new ThreadStart(e.Read));
            rt2.Start();

            Console.ReadLine();
        }

        private void Write()
        {
            int a = 10;
            int b = 11;
            Console.WriteLine("************** Write *************");

            for (int i = 0; i < 5; i++)
            {
                this.rw.WriteInts(a++, b++);
                Thread.Sleep(1000);
            }
        }

        private void Read()
        {
            int a = 10;
            int b = 11;
            Console.WriteLine("************** Read *************");

            for (int i = 0; i < 5; i++)
            {
                this.rw.ReadInts(ref a, ref b);
                Console.WriteLine("For i = " + i
                    + " a = " + a
                    + " b = " + b
                    + " TheadID = " + Thread.CurrentThread.GetHashCode());
                Thread.Sleep(1000);
            }
        }
    }
}

ReadWriteLock 的輸出結果可能與下表類似:

技術分享圖片

在上面的列表中,線程wt1 和 wt2 是WriteInts()方法中獲得寫鎖的寫者線程,線程rt1 和 rt2 是在ReadInts()方法中獲得讀者鎖的讀者線程。在WriteInts()方法中,變量x 和 y 的值分別被改成a 和 b. 當線程wt1 或 wt2 通過調用AcquireWriterLock() 方法獲得一個寫者鎖後,那麽直到這個線程通過調用ReleaseWriterLock()方法釋放鎖之前任何其他線程(包括讀者線程rt1 和 rt2)都不被允許訪問相應對象。這個行為與Monitors類似 。在ReadInts()方法中,線程rt1 和 rt2 通過調用AcquireReaderLock()方法獲得讀者鎖, 這兩個線程可以並發地訪問變量x 和 y. 直到讀者線程釋放它們的讀者鎖以後,寫者線程(wt1 和 wt2)才被允許訪問對應對象。只有讀者線程在獲得讀者鎖以後才可以並發地訪問。

Monitors類對於只想來讀數據而非寫數據來說過於“安全”了。Monitors 也有一些性能問題,對於只讀類型的訪問來說,性能瓶頸是可以避免的。ReaderWriterLock類通過允許任意數量的線程並發地讀取數據來提供一個解決數據讀-寫問題的完美方案。當線程更新數據時鎖住數據。當沒有寫者線程擁有鎖的時候寫者線程可以獲得鎖。寫者鎖可以在沒有讀者線程或者寫者線程擁有鎖的時候獲得鎖。因此,ReaderWriterLock 就像是一段關鍵部分代碼, 它也支持一個timeout 值,而這方面在檢測死鎖時非常有用。

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);
}
}

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

C# 多線程並發鎖模式-總結