在.Net框架中 C# 實現多執行緒的同步方法詳解
本文主要描述在C#
中執行緒同步的方法。執行緒的基本概念網上資料也很多就不再贅述了。直接接入主題,在多執行緒開發的應用中,執行緒同步是不可避免的。在.Net
框架中,實現執行緒同步主要通過以下的幾種方式來實現,在MSDN的執行緒指南中已經講了幾種,本文結合作者實際中用到的方式一起說明一下。
- 維護自由鎖(
InterLocked
)實現同步 - 監視器(
Monitor
)和互斥鎖(lock
) - 讀寫鎖(
ReadWriteLock
) - 系統核心物件
- 互斥(
Mutex
), 訊號量(Semaphore
), 事件(AutoResetEvent/ManualResetEvent
) - 執行緒池
- 互斥(
除了以上的這些物件之外實現執行緒同步的還可以使用Thread.Join
方法。這種方法比較簡單,當你在第一個執行緒執行時想等待第二個執行緒執行結果,那麼你可以讓第二個執行緒Join
進來就可以了。
自由鎖(InterLocked
)
對一個32位的整型數進行遞增和遞減操作來實現鎖,有人會問為什麼不用++
或--
來操作。因為在多執行緒中對鎖進行操作必須是原子的,而++
和--
不具備這個能力。InterLocked
類還提供了兩個另外的函式Exchange
, CompareExchange
用於實現交換和比較交換。Exchange
操作會將新值設定到變數中並返回變數的原來值: int oVal = InterLocked.Exchange(ref val, 1)
。
監視器(Monitor
)
在MSDN中對Monitor
的描述是: Monitor
類通過向單個執行緒授予物件鎖來控制對物件的訪問。
Monitor
類是一個靜態類因此你不能通過例項化來得到類的物件。Monitor
的成員可以檢視MSDN,基本上Monitor
的效果和lock
是一樣的,通過加鎖操作Enter
設定臨界區,完成操作後使用Exit
操作來釋放物件鎖。不過相對來說Monitor
的功能更強,Moniter
可以進行測試鎖的狀態,因此你可以控制對臨界區的訪問選擇,等待or離開, 而且Monitor
還可以在釋放鎖之前通知指定的物件,更重要的是使用Monitor
可以跨越方法來操作。Monitor
提供的方法很少就只有獲取鎖的方法Enter
, TryEnter
;釋放鎖的方法Wait
, Exit
;還有訊息通知方法Pulse
, PulseAll
。經典的Monitor
操作是這樣的:
// 通監視器來建立臨界區
static public void DelUser(string name)
{
try
{
// 等待執行緒進入
Monitor.Enter(Names);
Names.Remove(name);
Console.WriteLine("Del: {0}", Names.Count);
Monitor.Pulse(Names);
}
finally
{
// 釋放物件鎖
Monitor.Exit(Names);
}
}
}
其中Names
是一個List
, 這裡有一個小技巧,如果你想宣告整個方法為執行緒同步可以使用方法屬性:
// 通過屬性設定整個方法為臨界區
[MethodImpl(MethodImplOptions.Synchronized)]
static public void AddUser(string name)
{
Names.Add(name);
Console.WriteLine("Add: {0}",Names.Count);
}
對於Monitor
的使用有一個方法是比較詭異的,那就是Wait
方法。在MSDN中對Wait
的描述是: 釋放物件上的鎖以便允許其他執行緒鎖定和訪問該物件。
這裡提到的是先釋放鎖,那麼顯然我們需要先得到鎖,否則呼叫Wait
會出現異常,所以我們必須在Wait
前面呼叫Enter
方法或其他獲取鎖的方法,如lock
,這點很重要。對應Enter
方法,Monitor
給出來另一種實現TryEnter
。這兩種方法的主要區別在於是否阻塞當前執行緒,Enter
方法在獲取不到鎖時,會阻塞當前執行緒直到得到鎖。不過缺點是如果永遠得不到鎖那麼程式就會進入死鎖狀態。我們可以採用Wait
來解決,在呼叫Wait
時加入超時時限就可以。
if (Monitor.TryEnter(Names))
{
Monitor.Wait(Names, 1000); // !!
Names.Remove(name);
Console.WriteLine("Del: {0}", Names.Count);
Monitor.Pulse(Names);
}
互斥鎖(lock
)
lock
關鍵字是實現執行緒同步的比較簡單的方式,其實就是設定一個臨界區。在lock
之後的{...}
區塊為一個臨界區,當進入臨界區時加互斥鎖,離開臨界區時釋放互斥鎖。MSDN對lock
關鍵字的描述是: lock
關鍵字可將語句塊標記為臨界區,方法是獲取給定物件的互斥鎖,執行語句,然後釋放該鎖。
具體例子如下:
static public void ThreadFunc(object name)
{
string str = name as string;
Random rand = new Random();
int count = rand.Next(100, 200);
for (int i = 0; i < count; i++)
{
lock (NumList)
{
NumList.Add(i);
Console.WriteLine("{0} {1}", str, i);
}
}
}
對lock
的使用有幾點建議:對例項鎖定lock(this)
,對靜態變數鎖定lock(typeof(val))
。lock
的物件訪問許可權最好是private
,否則會出現失去訪問控制現象。
讀寫鎖(ReadWriteLock
)
讀寫鎖的出現主要是在很多情況下,我們讀資源的操作要多於寫資源的操作。但是如果每次只對資源賦予一個執行緒的訪問許可權顯然是低效的,讀寫鎖的優勢是同時可以有多個執行緒對同一資源進行讀操作。因此在讀操作比寫操作多很多,並且寫操作的時間很短的情況下使用讀寫鎖是比較有效率的。讀寫鎖是一個非靜態類所以你在使用前需要先宣告一個讀寫鎖物件:
static private ReaderWriterLock _rwlock = new ReaderWriterLock();
讀寫鎖是通過呼叫AcquireReaderLock
,ReleaseReaderLock
,AcquireWriterLock
,ReleaseWriterLock
來完成讀鎖和寫鎖控制的
static public void ReaderThread(int thrdId)
{
try
{ // 請求讀鎖,如果100ms超時退出
_rwlock.AcquireReaderLock(10);
try
{
int inx = _rand.Next(_list.Count);
if (inx < _list.Count)
Console.WriteLine("{0}thread {1}", thrdId, _list[inx]);
}
finally
{
_rwlock.ReleaseReaderLock();
}
}
catch (ApplicationException) // 如果請求讀鎖失敗
{
Console.WriteLine("{0}thread get reader lock out time!", thrdId);
}
}
static public void WriterThread()
{
try
{
// 請求寫鎖
_rwlock.AcquireWriterLock(100);
try
{
string val = _rand.Next(200).ToString();
_list.Add(val); // 寫入資源
Console.WriteLine("writer thread has written {0}", val);
}
finally
{ // 釋放寫鎖
_rwlock.ReleaseWriterLock();
}
}
catch (ApplicationException)
{
Console.WriteLine("Get writer thread lock out time!");
}
}
如果你想在讀的時候插入寫操作請使用UpgradeToWriterLock
和DowngradeFromWriterLock
來進行操作,而不是釋放讀鎖。
static private void UpgradeAndDowngrade(int thrdId)
{
try
{
_rwlock.AcquireReaderLock(10);
try
{
try
{
// 提升讀鎖到寫鎖
LockCookie lc = _rwlock.UpgradeToWriterLock(100);
try
{
string val = _rand.Next(500).ToString();
_list.Add(val); Console.WriteLine
("Upgrade Thread{0} add {1}", thrdId, val);
}
finally
{ // 下降寫鎖
_rwlock.DowngradeFromWriterLock(ref lc);
}
}
catch (ApplicationException)
{
Console.WriteLine("{0}thread upgrade reader lock failed!", thrdId);
}
}
finally
{
// 釋放原來的讀鎖
_rwlock.ReleaseReaderLock();
}
}
catch (ApplicationException)
{
Console.WriteLine("{0}thread get reader lock out time!", thrdId);
}
}
這裡有一點要注意的就是讀鎖和寫鎖的超時等待時間間隔的設定。通常情況下設定寫鎖的等待超時要比讀鎖的長,否則會經常發生寫鎖等待失敗的情況。
系統核心物件 互斥物件(Mutex
)
互斥物件的作用有點類似於監視器物件,確保一個程式碼塊在同一時刻只有一個執行緒在執行。互斥物件和監視器物件的主要區別就是,互斥物件一般用於跨程式間的執行緒同步,而監視器物件則用於程式內的執行緒同步。互斥物件有兩種:一種是命名互斥;另一種是匿名互斥。在跨程式中使用到的就是命名互斥,一個已命名的互斥就是一個系統級的互斥,它可以被其他程式所使用,只要在建立互斥時指定開啟互斥的名稱就可以。在.Net
中互斥是通過Mutex
類來實現。
其實對於OpenExisting
函式有兩個過載版本,
Mutex.OpenExisting (String)
Mutex.OpenExisting (String, MutexRights)
對於預設的第一個函式其實是實現了第二個函式 MutexRights.Synchronize|MutexRights.Modify
操作。
由於監視器的設計是基於.Net
框架,而Mutex
類是系統核心物件封裝了win32
的一個核心結構來實現互斥,並且互斥操作需要請求中斷來完成,因此在進行程式內執行緒同步的時候效能上要比互斥要好。
典型的使用Mutex
同步需要完成三個步驟的操作:
1.開啟或者建立一個Mutex
例項;
2.呼叫WaitOne()
來請求互斥物件;
3.最後呼叫ReleaseMutex
來釋放互斥物件。
static public void AddString(string str)
{
// 設定超時時限並在wait前退出非預設託管上下文
if (_mtx.WaitOne(1000, true))
{
_resource.Add(str);
_mtx.ReleaseMutex();
}
}
需要注意的是,WaitOne
和ReleaseMutex
必須成對出現,否則會導致程式死鎖的發生,這時系統(.Net2.0)框架會丟擲AbandonedMutexException
異常。
訊號量(Semaphore
)
訊號量就像一個夜總會:它有確切的容量,並被保鏢控制。一旦滿員,就沒有人能再進入,其他人必須在外面排隊。那麼在裡面離開一個人後,隊頭的人就可以進入。訊號量的建構函式需要提供至少兩個引數-現有的人數和最大的人數。
訊號量的行為有點類似於Mutex
或是lock
,但是訊號量沒有擁有者。任意執行緒都可以呼叫Release
來釋放訊號量而不像Mutex
和lock
那樣需要執行緒得到資源才能釋放。
class SemaphoreTest
{
static Semaphore s = new Semaphore(3, 3); // 當前值=3; 容量=3
static void Main()
{
for (int i = 0; i < 10; i++)
new Thread(Go).Start();
}
static void Go()
{
while (true)
{
s.WaitOne();
Thread.Sleep(100); // 一次只有個執行緒能被處理
s.Release();
}
}
}
事件(ManualResetEvent
/AutoResetEvent
)
AutoResetEvent
一個AutoResetEvent
象是一個"檢票輪盤":插入一張通行證然後讓一個人通過。"auto"的意思就是這個"輪盤"自動關閉或者開啟讓某人通過。執行緒將在呼叫WaitOne
後進行等待或者是阻塞,並且通過呼叫Set
操作來插入執行緒。如果一堆執行緒呼叫了WaitOne
操作,那麼"輪盤"就會建立一個等待佇列。一個通行證可以來自任意一個執行緒,換句話說任意一個執行緒都可以通過訪問AutoResetEvent
物件並呼叫Set
來釋放一個阻塞的執行緒。
如果在Set
被呼叫的時候沒有執行緒等待,那麼控制程式碼就會一直處於開啟狀態直到有執行緒呼叫了WaitOne
操作。這種行為避免了競爭條件-當一個執行緒還沒來得急釋放而另一個執行緒就開始進入的情況。因此重複的呼叫Set
操作一個"輪盤"哪怕是沒有等待執行緒也不會一次性的讓所有執行緒進入。
WaitOne
操作接受一個超時引數-當發生等待超時的時候,這個方法會返回一個false
。當已有一個執行緒在等待的時候,WaitOne
操作可以指定等待還是退出當前同步上下文。Reset
操作提供了關閉"輪盤"的操作。
AutoResetEvent
能夠通過兩個方法來建立:
1.呼叫建構函式 EventWaitHandle wh = new AutoResetEvent (false);
如果boolean
值為true
,那麼控制程式碼的Set
操作將在建立後自動被呼叫 ;
2. 通過基類EventWaitHandle
方式 EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto);
EventWaitHandle
建構函式允許建立一個ManualResetEvent
。人們應該通過呼叫Close
來釋放一個Wait Handle
在它不再使用的時候。當在應用程式的生存期內Wait handle
繼續被使用,那麼如果遺漏了Close
這步,在應用程式關閉的時候也會被自動釋放。
class BasicWaitHandle
{
static EventWaitHandle wh = new AutoResetEvent(false);
static void Main()
{
new Thread(Waiter).Start();
Thread.Sleep(1000); // 等待一會兒
wh.Set(); // 喚醒
}
static void Waiter()
{
Console.WriteLine("Waiting...");
wh.WaitOne(); // 等待喚醒
Console.WriteLine("Notified");
}
}
ManualResetEvent
ManualResetEvent
是AutoResetEvent
的一個特例。它的不同之處在於線上程呼叫WaitOne
後不會自動的重置狀態。它的工作機制有點象是開關:呼叫Set
開啟並允許其他執行緒進行WaitOne
;呼叫Reset
關閉那麼排隊的執行緒就要等待,直到下一次開啟。可以使用一個帶volatile
宣告的boolean
欄位來模擬間斷休眠 - 通過重複檢測標誌,然後休眠一小段時間。
ManualResetEvent
常常被用於協助完成一個特殊的操作,或者讓一個執行緒在開始工作前完成初始化。
執行緒池(Thread Pooling
)
如果你的應用程式擁有大量的執行緒並花費大量的時間阻塞在一個Wait Handle
上,那麼你要考慮使用執行緒池(Thead pooling
)來處理。執行緒池通過合併多個Wait Handle
來節約等待的時間。當Wait Handle
被啟用時,使用執行緒池你需要註冊一個Wait Handle
到一個委託去執行。通過呼叫ThreadPool.RegisterWaitForSingleObject
方法:
class Test
{
static ManualResetEvent starter = new ManualResetEvent(false);
public static void Main()
{
ThreadPool.RegisterWaitForSingleObject(starter, Go, "hello", -1, true);
Thread.Sleep(5000);
Console.WriteLine("Signaling worker...");
starter.Set();
Console.ReadLine();
}
public static void Go(object data, bool timedOut)
{
Console.WriteLine("Started " + data); // Perform task...
}
}
對於Wait Handle
和委託,RegisterWaitForSingleObject
接受一個"黑盒"物件並傳遞給你的委託(就像ParameterizedThreadStart
),超時設定和boolean
標誌指示了關閉和迴圈的請求。所有進入池中的執行緒都被認為是後臺執行緒,這就意味著它們不再由應用程式控制,而是由系統控制直到應用程式退出。
注意:如果這時候呼叫Abort
操作,可能會發生意想不到的情況。
你也可以通過呼叫QueueUserWorkItem
方法使用執行緒池,指定委託並立即被執行。這時你不能在多工情況下儲存共享執行緒,但是可以得到另外的好處:執行緒池會保持一個執行緒的總容量,當作業數超出容量時自動插入任務。
class Test
{
static object workerLocker = new object();
static int runningWorkers = 100;
public static void Main()
{
for (int i = 0; i < runningWorkers; i++)
{
ThreadPool.QueueUserWorkItem(Go, i);
}
Console.WriteLine("Waiting for threads to complete...");
lock (workerLocker)
{
while (runningWorkers > 0)
Monitor.Wait(workerLocker);
}
Console.WriteLine("Complete!");
Console.ReadLine();
}
public static void Go(object instance)
{
Console.WriteLine("Started: " + instance);
Thread.Sleep(1000);
Console.WriteLine("Ended: " + instance);
lock (workerLocker)
{
runningWorkers--;
Monitor.Pulse(workerLocker);
}
}
}
為了傳遞多個物件到目標方法,你必須定義一個客戶物件幷包含所有屬性或通過呼叫非同步的委託。如Go
方法接受兩引數:
ThreadPool.QueueUserWorkItem (delegate (object notUsed) { Go (23,34); });
其他的方法可以使用非同步委託。
.net多執行緒同步方式總結
在多執行緒開發中,共享物件的同步是經常遇到的問題,以下總結了C#中執行緒同步的幾種技術:
1,InterLocked
原子操作
Decrement(ref int location);遞減1
Add(ref int location1, int value);location1+value
Increment(ref int location);遞增1
2,Mutex
互斥鎖
WaitOne(int timeout);等待獲取鎖
ReleaseMutex();釋放鎖,記得一定要釋放,否則永遠被阻塞
3,SemaphoreSlim
訊號量
SemaphoreSlim(int count);允許的併發執行緒數量
Wait();等待
Release();釋放
4,AutoRestEvent
自動重置時間(核心模式)
Set();從一個執行緒向另一個執行緒傳送通知;
WaitOne();等待通知
5,ManaulResetEventSlim
手動重置時間(混合模式)
Wait();等待
Set();通知
Reset();重置
6,CountDownEvent
計數事件
CountdownEvent(int count);通知計數
Signal();計數(執行緒完成一定呼叫)
Wait();等待
Dispose();釋放
7,Barrier
Barrier(int participantCount, Action<Barrier> postPhaseAction);多個執行緒同步,回撥Action
SignalAndWait();執行回撥
8,ReaderWriterLockSlim
讀寫鎖
EnterReadLock();獲取讀鎖(可共享讀)
ExitReadLock();釋放讀鎖
EnterUpgradeableReadLock();獲取讀鎖(可升級到寫鎖);
ExitUpgradeableReadLock();釋放升級讀鎖
EnterWriteLock();獲取寫鎖(其他執行緒不可讀寫)
ExitWriteLock();釋放寫鎖
9,SpinWait
自旋等待(混合模式)
SpinOnce();自旋
相關文章
- Java中的執行緒同步詳解Java執行緒
- JAVA多執行緒詳解(3)執行緒同步和鎖Java執行緒
- C#多執行緒開發-執行緒同步 02C#執行緒
- Java中的多執行緒詳解Java執行緒
- 詳解多執行緒執行緒
- 多執行緒詳解執行緒
- 多執行緒和多執行緒同步執行緒
- C#多執行緒程式設計-基元執行緒同步構造C#執行緒程式設計
- C#多執行緒(4):程式同步Mutex類C#執行緒Mutex
- iOS多執行緒詳解:實踐篇iOS執行緒
- Java多執行緒詳解Java執行緒
- iOS 多執行緒詳解iOS執行緒
- 多執行緒03:?執行緒傳參詳解執行緒
- .NET中各種執行緒同步鎖執行緒
- 掌握C#中的GUI多執行緒技巧:WinForms和WPF例項詳解C#GUI執行緒ORM
- Java多執行緒【三種實現方法】Java執行緒
- Java實現多執行緒詳解一 ( 繼承Thread方式 )Java執行緒繼承thread
- 執行緒同步方法執行緒
- Flutter非同步與執行緒詳解Flutter非同步執行緒
- Java同步之執行緒池詳解Java執行緒
- 在 C++ 中,實現執行緒同步主要有以下幾種常見方法C++執行緒
- C#多執行緒(6):執行緒通知C#執行緒
- iOS多執行緒:NSOperation詳解iOS執行緒
- iOS多執行緒:GCD詳解iOS執行緒GC
- JAVA多執行緒詳解(一)Java執行緒
- Java 多執行緒詳解(一)Java執行緒
- Java多執行緒超詳解Java執行緒
- Android 多執行緒-----AsyncTask詳解Android執行緒
- java 多執行緒 –同步Java執行緒
- java 多執行緒 --同步Java執行緒
- Java多執行緒的實現Java執行緒
- 多執行緒併發:以AQS中acquire()方法為例來分析多執行緒間的同步與協作執行緒AQSUI
- java多執行緒與併發 - 執行緒池詳解Java執行緒
- .NET Core 中使用多執行緒或非同步操作來實現分片下載執行緒非同步
- c#關於同步 /異常/多執行緒/事件 事例C#執行緒事件
- 多執行緒(五)---執行緒的Yield方法執行緒
- C#多執行緒程式設計實戰1.1建立執行緒C#執行緒程式設計
- 如何實現多執行緒執行緒