VS.NET多線程式控制制語句互斥
1、Monitor.Enter和Monitor.Exit
Monitor 類通過向單個線程授予對象鎖來控制對對象的訪問。對象鎖提供限制存取碼塊(通常稱為臨界區)的能力。當一個線程擁有對象的鎖時,其他任何線程都不能擷取該鎖。還可以使用 Monitor 來確保不會允許其他任何線程訪問正在由鎖的所有者執行的應用程式代碼節,除非另一個線程正在使用其他的鎖定對象執行該代碼。
注意:使用 Monitor 鎖定對象(即參考型別)而不是實值型別。
Monitor 具有以下功能:
它根據需要與某個對象相關聯。
它是未綁定的,也就是說可以直接從任何上下文調用它。
不能建立 Monitor 類的執行個體。
將為每個同步對象來維護以下資訊:
對當前持有鎖的線程的引用。
對就緒隊列的引用,它包含準備擷取鎖的線程。
對等待隊列的引用,它包含正在等待鎖定對象狀態變化通知的線程。
使用 Enter 和 Exit 方法標記臨界區的開頭和結尾。
如果臨界區是一個連續指令集,則由 Enter 方法擷取的鎖將保證只有一個線程可以使用鎖定對象執行所包含的代碼。在這種情況下,建議您將這些指令放在 try 塊中,並將 Exit 指令放在 finally 塊中。此功能通常用於同步對類的靜態或執行個體方法的訪問。
如果執行個體方法需要同步線程訪問,則它將使用當前執行個體作為要鎖定的對象調用 Enter 和對應的 Exit 方法。由於只能有一個線程持有當前執行個體上的鎖,因此該方法一次只能由一個線程來執行。
靜態方法是使用當前執行個體的 Type 作為鎖定對象以類似的方式來保護的。Enter 和 Exit 方法提供的功能與 C# lock 語句提供的功能相同。
如果臨界區跨越整個方法,則可以通過將 System.Runtime.CompilerServices.MethodImplAttribute 放置在方法上並在 MethodImplAttribute 的建構函式中指定 Synchronized 值來實現上述鎖定功能。使用該屬性後就不需要 Enter 和 Exit 語句了。請注意,該屬性將使當前線程持有鎖,直到方法返回;如果可以更早釋放鎖,則使用 Monitor 類或 C# lock 語句而不是該屬性。
儘管鎖定和釋放給定對象的 Enter 和 Exit 語句可以跨越成員或類的邊界或同時跨越兩者的邊界,但並不推薦這樣做。
當選擇要同步的對象時,應只鎖定私人或內部對象。鎖定外部對象可能導致死結,這是因為不相關的代碼可能會出於不同的目的而選擇鎖定相同的對象。
//執行個體方法同步線程訪問
public class Account
{
int val;
public void Deposit(int x)
{
Monitor.Enter(this);
try
{
val += x;
}
finally
{
Monitor.Exit(this);
}
}
public void WithDraw(int x)
{
Monitor.Exiter(this);
try
{
val -= x;
}
finally
{
Monitor.Exit(this);
}
}
}
//靜態方法同步線程訪問
public class DemoStatic
{
public static int count =0;
}
public class DemoStaticLock
{
public static void Demo()
{
try
{
Monitor.Enter(typeof(DemoStatic));
DemoStatic.count++;
}
catch(Exception e)
{
Console.WriteLine("捕獲異常{0}",e.ToString());
}
finally
{
Monitor.Exit(typeof(DemoStatic));
}
}
}
2、Lock/SyncLock語句
Lock/SyncLock關鍵字將某個語句標誌為臨界區。
//執行個體方法同步線程訪問
public class Account
{
int val;
public void Deposit(int x)
{
lock(this)
{
val += x;
}
}
public void WithDraw(int x)
{
lock(this)
{
val -= x;
}
}
}
3、ReaderWriterLock
ReaderWriterLock 用於同步對資源的訪問。在任一特定時刻,它允許多個線程同時進行讀訪問,或者允許單個線程進行寫訪問。在資源不經常發生更改的情況下,ReaderWriterLock 所提供的輸送量比簡單的一次只允許一個線程的鎖(如 Monitor)更高。
在多數訪問為讀訪問,而寫訪問頻率較低、期間也比較短的情況下,ReaderWriterLock 的效能最好。多個讀線程與單個寫線程交替進行操作,所以讀線程和寫線程都不會長時間阻塞。
注意:長時間持有讀線程鎖或寫線程鎖會使其他線程發生饑餓 (starve)。為了得到最好的效能,需要考慮重新構造應用程式以將寫訪問的期間減少到最小。
一個線程可以持有讀線程鎖或寫線程鎖,但是不能同時持有兩者。若要擷取寫線程鎖,請使用 UpgradeToWriterLock 和 DowngradeFromWriterLock,而不要通過釋放讀線程鎖的方式擷取。
遞迴鎖請求會增加鎖上的鎖計數。
讀線程和寫線程將分別排入各自的隊列。當線程釋放寫線程鎖時,此刻讀線程隊列中的所有等待線程都將被授予讀線程鎖;當已釋放所有讀線程鎖時,寫線程隊列中處於等待狀態的下一個線程(如果存在)將被授予寫線程鎖,依此類推。換句話說,ReaderWriterLock 在一組讀線程和一個寫線程之間交替進行操作。
當寫線程隊列中有一個線程在等待活動讀線程鎖被釋放時,請求新的讀線程鎖的線程會排入讀線程隊列。即使它們能和現有的讀線程鎖持有人同時一起訪問,也不會授予它們的請求;這可以防止寫線程被讀線程無限期阻塞。
大多數在 ReaderWriterLock 上擷取鎖的方法都採用逾時值。使用逾時可以避免應用程式中出現死結。例如,某個線程可能擷取了一個資源上的寫線程鎖,然後請求第二個資源上的讀線程鎖;同時,另一個線程擷取了第二個資源上的寫線程鎖,並請求第一個資源上的讀線程鎖。如果不使用逾時,這兩個線程將出現死結。
如果逾時間隔到期並且沒有授予鎖請求,則此方法通過引發 ApplicationException 將控制返回給調用線程。線程可以捕捉此異常並確定下一步要進行的操作。
逾時用毫秒錶示。如果使用 System.TimeSpan 指定逾時,則所用的值是 TimeSpan 所表示的毫秒整數的總和。
下面顯示用毫秒錶示的有效逾時值。
值 說明
-1 Infinite.
0 無逾時。
> 0 要等待的毫秒數。
除了 -1 以外,不允許使用負的逾時值。如果要使用 -1 以外的負整數來指定逾時,系統將使用零(無逾時)。如果指定的 TimeSpan 表示的是 -1 以外的負毫秒數,將引發 ArgumentOutOfRangeException。
使用ReaderWriterLock實現互斥:首先執行個體化ReaderWriterLock;然後在讀取臨界資源前調用AcquireReaderLock方法,讀過程結束後,調用ReleaseReaderLock釋放讀鎖定;在修改臨界資源之前,調用AcquireWriterLock方法請求寫鎖定,在寫過程結束後,調用ReleaseWriterLock方法釋放寫鎖定。
//例子
public class Account
{
int val ;
ReaderWriterLock rwl = new ReaderWriterLock();
public int Read()
{
rwl.AcquireReaderLock(Timeout.Infinite);
int iRet = 0;
try
{
iRet = val;
}
finally
{
rwl.ReleaseReaderLock();
}
return iRet;
}
public void Deposit(int x)
{
rwl.AcquireWriterLock(Timeout.Infinite);
try
{
val += x;
}
finally
{
rwl.ReleaseWriterLock();
}
}
}
4、互斥體Mutex
當兩個或更多線程需要同時訪問一個共用資源時,系統需要使用同步機制來確保一次只有一個線程使用該資源。Mutex 是同步基元,它只向一個線程授予對共用資源的獨佔訪問權。如果一個線程擷取了互斥體,則要擷取該互斥體的第二個線程將被掛起,直到第一個線程釋放該互斥體。
可以使用 WaitHandle.WaitOne 請求互斥體的所屬權。擁有互斥體的線程可以在對 Wait 的重複調用中請求相同的互斥體而不會阻塞其執行。但線程必須調用 ReleaseMutex 方法同樣多的次數以釋放互斥體的所屬權。如果線程在擁有互斥體期間正常終止,則互斥體狀態設定為終止,並且下一個等待線程獲得所屬權。如果沒有線程擁有互斥體,則互斥體狀態為終止。
public class Account
{
int val = 100;
Mutex m = new Mutex();
public void Deposite(int x)
{
m.WaitOne() ; //請求獲得互斥對象
try
{
val += x;
}
finally
{
m.ReleaseMutex(); //釋放互斥對象
}
}
public void Withdraw(int x)
{
m.WaitOne() ;
try
{
val -= x;
}
finally
{
m.ReleaseMutex();
}
}
}
可以使用Mutex對象線上程之間跨進程進行同步。雖然Mutex不具備Monitor類的所有等待和脈衝功能,但它的確提供了建立可在進程之間使用的命名的互斥的功能。如下代碼:
using System;
using System.Threading;
namespace DemoSyncAcrossProc
{
public class App
{
static public void DemoMutex()
{
Mutex mutex = new Mutex(false, "Demo");
Console.WriteLine("建立名為Demo的互斥體");
if(mutex.WaitOne())
Console.WriteLine("得到互斥體");
else
Console.WriteLine("沒有得到互斥體");
Console.WriteLine("按任意鍵退出");
Console.ReadLine();
}
static int Main()
{
DemoMutex();
return 0;
}
}
}
編譯後,開啟一個命令視窗,運行該程式,不按任何鍵,程式顯示如下:
建立名為Demo的互斥體
得到互斥體
按任意鍵退出
再開啟第2個命令視窗執行該程式,程式顯示如下:
建立名為Demo的互斥體
此時,第2個執行個體被阻塞(因為第1個應用執行個體還沒釋放名為Demo的互斥體)。
接著切換到第1個命令列視窗,按任意鍵結束第1個應用執行個體,再切換到第2個命令列視窗,則會觀測到程式繼續執行輸出“得到互斥體”。
5、InterLocked
此類的方法可以防止可能在下列情況發生的錯誤:線程正在更新可由其他線程訪問的變數時,排程器切換上下文;或者兩個線程在不同的處理器上同時執行。此類的成員不引發異常。
Increment 和 Decrement 方法遞增或遞減變數並將結果值儲存在單個操作中。在大多數電腦上,增加變數操作不是一個原子操作,需要執行下列步驟:
(1).將執行個體變數中的值載入到寄存器中。
(2).增加或減少該值。
(3).在執行個體變數中儲存該值。
如果不使用 Increment 和 Decrement,線程會在執行完前兩個步驟後被搶先。然後由另一個線程執行所有三個步驟。當第一個線程重新開始執行時,它改寫執行個體變數中的值,造成第二個線程執行增減操作的結果丟失。
Exchange 方法自動交換指定變數的值。CompareExchange 方法組合了兩個操作:比較兩個值以及根據比較的結果將第三個值儲存在其中一個變數中。比較和交換操作按原子操作執行。
有InterLocked公開的Exchange和CompareExchange方法採用可以儲存引用的Object類型的參數。但是,型別安全要求將所有參數嚴格類型化為Object;不能對其中一個方法的調用中簡單地將對象強制轉換為Object,換言之,必須建立Object類型變數,將自訂對象賦給該變數,然後傳遞該變數。例如:
public class DemoInterLocked
{
Object _x;
public Object X
{
set
{
Object ovalue = value;
InterLocked.CompareExchange(ref _x , ovalue , null);
}
get
{
return _x;
}
}
}
//下面用Interlocked實現互斥的例子
using System ;
using System.Threading;
namespace DemoSyncResource
{
class Resource
{
ReaderWriterLock rwl = new ReaderWriterLock();
public void Read(Int32 threadNum)
{
rwl.AcquireReaderLock(Timeout.Infinite);
try
{
Console.WriteLine("開始讀資源(Thread={0})", threadNum);
Thread.Sleep(250);
Console.WriteLine("讀取資源結束(Thread={0})", threadNum);
}
finally
{
rwl.ReleaseReaderLock();
}
}
public void Write(Int32 threadNum)
{
rwl.AcquireWriterLock(Timeout.Infinite);
try
{
Console.WriteLine("開始寫資源 (Thread={0})", threadNum);
Thread.Sleep(750);
Console.WriteLine("寫資源結束 (Thread={0})", threadNum);
}
finally
{
rwl.ReleaseWriterLock();
}
}
}
class App
{
//臨界資源
static Int32 numAsyncOps = 4;
//同步對象
static AutoResetEvent asyncAreDone = new AutoResetEvent(false);
//臨界資源
static Resource res = new Resource();
public static void Main()
{
for (Int32 threadNum = 0 ;threadNum < 4 ; threadNum++)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(UpdateResource),threadNum);
}
asyncAreDone.WaitOne();
Console.WriteLine("所有操作都結束");
}
static void UpdateResource(object state)
{
Int32 threadNum = (Int32)state;
if ((threadNum % 2)!=0)
res.Read(threadNum);
else
res.Write(threadNum);
//利用Interlocked.Decrement互斥的修改臨界資源
//每執行一個線程都將numAsyncOps減1
//如果numAsyncOps變為0,說明4個線程都執行完了
if(Interlocked.Decrement(ref numAsyncOps) == 0)
asyncAreDone.Set();
}
}
}
這5種實現互斥方法的比較:
A.Monitor.Enter/Monitor.Exit和Lock(obj)/SyncLock都是基於引用對象瑣技術。鎖跟臨界資源捆綁在一起。這2種方法的粒度較粗。
B.Mutex是基於自身的鎖。通過將一個臨界資源跟一個Mutex執行個體相關,要求所有的請求該臨界資源的線程首先獲得跟它相關的Mutex鎖。這種方式的鎖定粒度可以自由控制,可以是一個對象、一段代碼、甚至整個過程。
C.Interlocked提供了基於粒度最細的鎖,它不依賴於鎖定,而基於原子操作的不可分割性,它使增、減、交換、比較等動作成為一個不可分割的原子操作實現互斥。