天天看點

c# 多線程

本文主要描述在C#中線程同步的方法。線程的基本概念網上資料也很多就不再贅述了。直接接入 主題,在多線程開發的應用中,線程同步是不可避免的。在.Net架構中,實作線程同步主要通過以下的幾種方式來實作,在MSDN的線程指南中已經講了幾 種,本文結合作者實際中用到的方式一起說明一下。

1. 維護自由鎖(InterLocked)實作同步

2. 螢幕(Monitor)和互斥鎖(lock)

3. 讀寫鎖(ReadWriteLock)

4. 系統核心對象

1) 互斥(Mutex), 信号量(Semaphore), 事件(AutoResetEvent/ManualResetEvent)

2) 線程池

除了以上的這些對象之外實作線程同步的還可以使用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操作是這樣的:

其中Names是一個List, 這裡有一個小技巧,如果你想聲明整個方法為線程同步可以使用方法屬性:

對于Monitor的使用有一個方法是比較詭異的,那就是Wait方法。在MSDN中對Wait的描述是: 釋放對象上的鎖以便允許其他線程鎖定和通路該對象。

這裡提到的是先釋放鎖,那麼顯然我們需要先得到鎖,否則調用Wait會出現異常,所 以我們必須在Wait前面調用Enter方法或其他擷取鎖的方法,如lock,這點很重要。對應Enter方法,Monitor給出來另一種實作 TryEnter。這兩種方法的主要差別在于是否阻塞目前線程,Enter方法在擷取不到鎖時,會阻塞目前線程直到得到鎖。不過缺點是如果永遠得不到鎖那 麼程式就會進入死鎖狀态。我們可以采用Wait來解決,在調用Wait時加入逾時時限就可以。

互斥鎖(lock)

lock關鍵字是實作線程同步的比較簡單的方式,其實就是設定一個臨界區。在 lock之後的{...}區塊為一個臨界區,當進入臨界區時加互斥鎖,離開臨界區時釋放互斥鎖。MSDN對lock關鍵字的描述是: lock 關鍵字可将語句塊标記為臨界區,方法是擷取給定對象的互斥鎖,執行語句,然後釋放該鎖。

具體例子如下:

對lock的使用有幾點建議:對執行個體鎖定lock(this),對靜态變量鎖定lock(typeof(val))。lock的對象通路權限最好是private,否則會出現失去通路控制現象。

讀寫鎖(ReadWriteLock)

讀寫鎖的出現主要是在很多情況下,我們讀資源的操作要多于寫資源的操作。但是如果每 次隻對資源賦予一個線程的通路權限顯然是低效的,讀寫鎖的優勢是同時可以有多個線程對同一資源進行讀操作。是以在讀操作比寫操作多很多,并且寫操作的時間 很短的情況下使用讀寫鎖是比較有效率的。讀寫鎖是一個非靜态類是以你在使用前需要先聲明一個讀寫鎖對象:

static private ReaderWriterLock _rwlock = new ReaderWriterLock();

讀寫鎖是通過調用AcquireReaderLock,ReleaseReaderLock,AcquireWriterLock,ReleaseWriterLock來完成讀鎖和寫鎖控制的

如果你想在讀的時候插入寫操作請使用UpgradeToWriterLock和DowngradeFromWriterLock來進行操作,而不是釋放讀鎖。

這裡有一點要注意的就是讀鎖和寫鎖的逾時等待時間間隔的設定。通常情況下設定寫鎖的等待逾時要比讀鎖的長,否則會經常發生寫鎖等待失敗的情況。

系統核心對象 互斥對象(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來釋放互斥對象。

需要注意的是,WaitOne和ReleaseMutex必須成對出現,否則會導緻程序死鎖的發生,這時系統(.Net2.0)架構會抛出AbandonedMutexException異常。

信号量(Semaphore)

信号量就像一個夜總會:它有确切的容量,并被保镖控制。一旦滿員,就沒有人能再進入,其他人必須在外面排隊。那麼在裡面離開一個人後,隊頭的人就可以進入。信号量的構造函數需要提供至少兩個參數-現有的人數和最大的人數。

信号量的行為有點類似于Mutex或是lock,但是信号量沒有擁有者。任意線程都可以調用Release來釋放信号量而不像Mutex和lock那樣需要線程得到資源才能釋放。

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這步,在應用程式關閉的時候也會被自動釋放。

ManualResetEvent

ManualResetEvent是AutoResetEvent的一個特例。它的 不同之處在于線上程調用WaitOne後不會自動的重置狀态。它的工作機制有點象是開關:調用Set打開并允許其他線程進行WaitOne;調用 Reset關閉那麼排隊的線程就要等待,直到下一次打開。可以使用一個帶volatile聲明的boolean字段來模拟間斷休眠 - 通過重複檢測标志,然後休眠一小段時間。

ManualResetEvent常常被用于協助完成一個特殊的操作,或者讓一個線程在開始工作前完成初始化。

線程池(Thread Pooling)

如果你的應用程式擁有大量的線程并花費大量的時間阻塞在一個Wait Handle上,那麼你要考慮使用線程池(Thead pooling)來處理。線程池通過合并多個Wait Handle來節約等待的時間。當Wait Handle被激活時,使用線程池你需要注冊一個Wait Handle到一個委托去執行。通過調用ThreadPool.RegisterWaitForSingleObject方法:

對于Wait Handle和委托,RegisterWaitForSingleObject接受一個"黑盒"對象并傳遞給你的委托(就像 ParameterizedThreadStart),逾時設定和boolean标志訓示了關閉和循環的請求。所有進入池中的線程都被認為是背景線程,這 就意味着它們不再由應用程式控制,而是由系統控制直到應用程式退出。

注意:如果這時候調用Abort操作,可能會發生意想不到的情況。

你也可以通過調用QueueUserWorkItem方法使用線程池,指定委托并立即被執行。這時你不能在多任務情況下儲存共享線程,但是可以得到另外的好處:線程池會保持一個線程的總容量,當作業數超出容量時自動插入任務。

為了傳遞多個對象到目标方法,你必須定義一個客戶對象并包含所有屬性或通過調用異步的委托。如Go方法接受兩參數:

ThreadPool.QueueUserWorkItem (delegate (object notUsed) { Go (23,34); });

其他的方法可以使用異步委托。