隨著對多線程學習的深入,你可能覺得需要瞭解一些有關線程共用資源的問題. .NET framework提供了很多的類和資料類型來控制對共用資源的訪問。
考慮一種我們經常遇到的情況:有一些全域變數和共用的類變數,我們需要從不同的線程來更新它們,可以通過使用System.Threading.Interlocked類完成這樣的任務,它提供了原子的,非模組化的整數更新操作。
還有你可以使用System.Threading.Monitor類鎖定對象的方法的一段代碼,使其暫時不能被別的線程訪問。
System.Threading.WaitHandle類的執行個體可以用來封裝等待對共用資源的獨佔訪問權的作業系統特定的對象。尤其對於非受管代碼的互操作問題。
System.Threading.Mutex用於對多個複雜的線程同步的問題,它也允許單線程的訪問。
像ManualResetEvent和AutoResetEvent這樣的同步事件類別支援一個類通知其他事件的線程。
不討論區對話的同步問題,等於對多線程編程知之甚少,但是我們要十分謹慎的使用多線程的同步。在使用線程同步時,我們事先就要要能夠正確的確定是那個對象和方法有可能造成死結(死結就是所有的線程都停止了相應,都在等者對方釋放資源)。還有贓資料的問題(指的是同一時間多個線程對資料作了操作而造成的不一致),這個不容易理解,這麼說吧,有X和Y兩個線程,線程X從檔案讀取資料並且寫資料到資料結構,線程Y從這個資料結構讀資料並將資料送到其他的電腦。假設在Y讀資料的同時,X寫入資料,那麼顯然Y讀取的資料與實際儲存的資料是不一致的。這種情況顯然是我們應該避免發生的。少量的線程將使得剛才的問題發生的幾率要少的多,對共用資源的訪問也更好的同步。
.NET Framework的CLR提供了三種方法來完成對共用資源 ,諸如全域變數域,特定的程式碼片段,靜態和執行個體化的方法和域。
(1) 代碼域同步:使用Monitor類可以同步靜態/執行個體化的方法的全部代碼或者部分程式碼片段。不支援靜態域的同步。在執行個體化的方法中,this指標用於同步;而在靜態方法中,類用於同步,這在後面會講到。
(2) 手工同步:使用不同的同步類(諸如WaitHandle, Mutex, ReaderWriterLock, ManualResetEvent, AutoResetEvent 和Interlocked等)建立自己的同步機制。這種同步方式要求你自己手動的為不同的域和方法同步,這種同步方式也可以用於進程間的同步和對共用資源的等待而造成的死結解除。
(3) 上下文同步:使用SynchronizationAttribute為ContextBoundObject對象建立簡單的,自動的同步。這種同步方式僅用於執行個體化的方法和域的同步。所有在同一個上下文域的對象共用同一個鎖。
Monitor Class
在給定的時間和指定的程式碼片段只能被一個線程訪問,Monitor 類非常適合於這種情況的線程同步。這個類中的方法都是靜態,所以不需要執行個體化這個類。下面一些靜態方法提供了一種機制用來同步對象的訪問從而避免死結和維護資料的一致性。
Monitor.Enter 方法:在指定對象上擷取獨佔鎖定。
Monitor.TryEnter 方法:試圖擷取指定對象的獨佔鎖定。
Monitor.Exit 方法:釋放指定對象上的獨佔鎖定。
Monitor.Wait 方法:釋放對象上的鎖並阻塞當前線程,直到它重新擷取該鎖。
Monitor.Pulse 方法:通知等待隊列中的線程鎖定對象狀態的更改。
Monitor.PulseAll 方法:通知所有的等待線程對象狀態的更改。
通過對指定對象的加鎖和解鎖可以同步程式碼片段的訪問。Monitor.Enter, Monitor.TryEnter 和 Monitor.Exit用來對指定對象的加鎖和解鎖。一旦擷取(調用了Monitor.Enter)指定對象(程式碼片段)的鎖,其他的線程都不能擷取該鎖。舉個例子來說吧,線程X獲得了一個對象鎖,這個對象鎖可以釋放的(調用Monitor.Exit(object) or Monitor.Wait)。當這個對象鎖被釋放後,Monitor.Pulse方法和 Monitor.PulseAll方法通知就緒隊列的下一個線程進行和其他所有就緒隊列的線程將有機會擷取獨佔鎖定。線程X釋放了鎖而線程Y獲得了鎖,同時調用Monitor.Wait的線程X進入等待隊列。當從當前鎖定對象的線程(線程Y)受到了Pulse或PulseAll,等待隊列的線程就進入就緒隊列。線程X重新得到對象鎖時,Monitor.Wait才返回。如果擁有鎖的線程(線程Y)不調用Pulse或PulseAll,方法可能被不確定的鎖定。Pulse, PulseAll and Wait必須是被同步的程式碼片段鄂被調用。對每一個同步的對象,你需要有當前擁有鎖的線程的指標,就緒隊列和等待隊列(包含需要被通知鎖定對象的狀態變化的線程)的指標。
你也許會問,當兩個線程同時調用Monitor.Enter會發生什麼事情?無論這兩個線程地調用Monitor.Enter是多麼地接近,實際上肯定有一個在前,一個在後,因此永遠只會有一個獲得對象鎖。既然Monitor.Enter是原子操作,那麼CPU是不可能偏好一個線程而不喜歡另外一個線程的。為了擷取更好的效能,你應該延遲後一個線程的擷取鎖調用和立即釋放前一個線程的對象鎖。對於private和internal的對象,加鎖是可行的,但是對於external對象有可能導致死結,因為不相關的代碼可能因為不同的目的而對同一個對象加鎖。
如果你要對一段代碼加鎖,最好的是在try語句裡面加入設定鎖的語句,而將Monitor.Exit放在finally語句裡面。對於整個程式碼片段的加鎖,你可以使用MethodImplAttribute(在System.Runtime.CompilerServices命名空間)類在其構造器中設定同步值。這是一種可以替代的方法,當加鎖的方法返回時,鎖也就被釋放了。如果需要要很快釋放鎖,你可以使用Monitor類和C# lock的聲明代替上述的方法。
讓我們來看一段使用Monitor類的代碼:
public void some_method()
{
int a=100;
int b=0;
Monitor.Enter(this);
//say we do something here.
int c=a/b;
Monitor.Exit(this);
}
上面的代碼運行會產生問題。當代碼運行到int c=a/b; 的時候,會拋出一個異常,Monitor.Exit將不會返回。因此這段程式將掛起,其他的線程也將得不到鎖。有兩種方法可以解決上面的問題。第一個方法是:將代碼放入try…finally內,在finally調用Monitor.Exit,這樣的話最後一定會釋放鎖。第二種方法是:利用C#的lock()方法。調用這個方法和調用Monitoy.Enter的作用效果是一樣的。但是這種方法一旦代碼執行超出範圍,釋放鎖將不會自動的發生。見下面的代碼:
public void some_method()
{
int a=100;
int b=0;
lock(this);
//say we do something here.
int c=a/b;
}
C# lock申明提供了與Monitoy.Enter和Monitoy.Exit同樣的功能,這種方法用在你的程式碼片段不能被其他獨立的線程中斷的情況。
WaitHandle Class
WaitHandle類作為基類來使用的,它允許多個等待操作。這個類封裝了win32的同步處理方法。WaitHandle對象通知其他的線程它需要對資源排他性的訪問,其他的線程必須等待,直到WaitHandle不再使用資源和等待控制代碼沒有被使用。下面是從它繼承來的幾個類:
Mutex 類:同步基元也可用於進程間同步。
AutoResetEvent:通知一個或多個正在等待的線程已發生事件。無法繼承此類。
ManualResetEvent:當通知一個或多個正在等待的線程事件已發生時出現。無法繼承此類。
這些類定義了一些訊號機制使得對資源排他性訪問的佔有和釋放。他們有兩種狀態:signaled 和 nonsignaled。Signaled狀態的等待控制代碼不屬於任何線程,除非是nonsignaled狀態。擁有等待控制代碼的線程不再使用等待控制代碼時用set方法,其他的線程可以調用Reset方法來改變狀態或者任意一個WaitHandle方法要求擁有等待控制代碼,這些方法見下面:
WaitAll:等待指定數組中的所有元素收到訊號。
WaitAny:等待指定數組中的任一元素收到訊號。
WaitOne:當在衍生類別中重寫時,阻塞當前線程,直到當前的 WaitHandle 收到訊號。
這些wait方法阻塞線程直到一個或者更多的同步對象收到訊號。
WaitHandle對象封裝等待對共用資源的獨佔訪問權的作業系統特定的對象無論是收管代碼還是非受管代碼都可以使用。但是它沒有Monitor使用輕便,Monitor是完全的受管代碼而且對作業系統資源的使用非常有效率。
Mutex Class
Mutex是另外一種完成線程間和跨進程同步的方法,它同時也提供進程間的同步。它允許一個線程獨佔共用資源的同時阻止其他線程和進程的訪問。Mutex的名字就很好的說明了它的所有者對資源的排他性的佔有。一旦一個線程擁有了Mutex,想得到Mutex的其他線程都將掛起直到佔有線程釋放它。Mutex.ReleaseMutex方法用於釋放Mutex,一個線程可以多次調用wait方法來請求同一個Mutex,但是在釋放Mutex的時候必須調用同樣次數的Mutex.ReleaseMutex。如果沒有線程佔有Mutex,那麼Mutex的狀態就變為signaled,否則為nosignaled。一旦Mutex的狀態變為signaled,等待隊列的下一個線程將會得到Mutex。Mutex類對應與win32的CreateMutex,建立Mutex對象的方法非常簡單,常用的有下面幾種方法:
一個線程可以通過調用WaitHandle.WaitOne 或 WaitHandle.WaitAny 或 WaitHandle.WaitAll得到Mutex的擁有權。如果Mutex不屬於任何線程,上述調用將使得線程擁有Mutex,而且WaitOne會立即返回。但是如果有其他的線程擁有Mutex,WaitOne將陷入無限期的等待直到擷取Mutex。你可以在WaitOne方法中指定參數即等待的時間而避免無限期的等待Mutex。調用Close作用於Mutex將釋放擁有。一旦Mutex被建立,你可以通過GetHandle方法獲得Mutex的控制代碼而給WaitHandle.WaitAny 或 WaitHandle.WaitAll 方法使用。
下面是一個樣本:
public void some_method()
{
int a=100;
int b=20;
Mutex firstMutex = new Mutex(false);
FirstMutex.WaitOne();
//some kind of processing can be done here.
Int x=a/b;
FirstMutex.Close();
}
在上面的例子中,線程建立了Mutex,但是開始並沒有申明擁有它,通過調用WaitOne方法擁有Mutex。
Synchronization Events
同步時間是一些等待控制代碼用來通知其他的線程發生了什麼事情和資源是可用的。他們有兩個狀態:signaled and nonsignaled。AutoResetEvent 和 ManualResetEvent就是這種同步事件。
AutoResetEvent Class
這個類可以通知一個或多個線程發生事件。當一個等待線程得到釋放時,它將狀態轉換為signaled。用set方法使它的執行個體狀態變為signaled。但是一旦等待的線程被通知時間變為signaled,它的轉檯將自動的變為nonsignaled。如果沒有線程偵聽事件,轉檯將保持為signaled。此類不能被繼承。
ManualResetEvent Class
這個類也用來通知一個或多個線程事件發生了。它的狀態可以手動的被設定和重設。手動重設時間將保持signaled狀態直到ManualResetEvent.Reset設定其狀態為nonsignaled,或保持狀態為nonsignaled直到ManualResetEvent.Set設定其狀態為signaled。這個類不能被繼承。
Interlocked Class
它提供了線上程之間共用的變數訪問的同步,它的操作時原子操作,且被線程共用.你可以通過Interlocked.Increment 或 Interlocked.Decrement來增加或減少共用變數.它的有點在於是原子操作,也就是說這些方法可以代一個整型的參數增量並且返回新的值,所有的操作就是一步.你也可以使用它來指定變數的值或者檢查兩個變數是否相等,如果相等,將用指定的值代替其中一個變數的值.
ReaderWriterLock class
它定義了一種鎖,提供唯一寫/多讀的機制,使得讀寫的同步.任意數目的線程都可以讀資料,資料鎖在有線程更新資料時將是需要的.讀的線程可以擷取鎖,若且唯若這裡沒有寫的線程.當沒有讀線程和其他的寫線程時,寫線程可以得到鎖.因此,一旦writer-lock被請求,所有的讀線程將不能讀取資料直到寫線程訪問完畢.它支援暫停而避免死結.它也支援嵌套的讀/寫鎖.支援嵌套的讀鎖的方法是ReaderWriterLock.AcquireReaderLock,如果一個線程有寫鎖則該線程將暫停;
支援嵌套的寫鎖的方法是ReaderWriterLock.AcquireWriterLock,如果一個線程有讀鎖則該線程暫停.如果有讀鎖將容易倒是死結.安全的辦法是使用ReaderWriterLock.UpgradeToWriterLock方法,這將使讀者升級到寫者.你可以用ReaderWriterLock.DowngradeFromWriterLock方法使寫者降級為讀者.調用ReaderWriterLock.ReleaseLock將釋放鎖, ReaderWriterLock.RestoreLock將重新裝載鎖的狀態到調用ReaderWriterLock.ReleaseLock以前.
結論:
這部分講述了.NET平台上的線程同步的問題.造接下來的系列文章中我將給出一些例子來更進一步的說明這些使用的方法和技巧.雖然線程同步的使用會給我們的程式帶來很大的價值,但是我們最好能夠小心使用這些方法.否則帶來的不是受益,而將倒是效能下降甚至程式崩潰.只有大量的聯絡和體會才能使你駕馭這些技巧.盡量少使用那些在同步代碼塊完成不了或者不確定的阻塞的東西,尤其是I/O操作;儘可能的使用局部變數來代替全域變數;同步用在那些部分代碼被多個線程和進程訪問和狀態被不同的進程共用的地方;安排你的代碼使得每一個資料在一個線程裡得到精確的控制;不是共用線上程之間的代碼是安全的;在下一篇文章中我們將學習線程池有關的知識.
如果你仔細閱讀了我前面的三篇文章,我相信你對用.NET Framework提供的System.Threading.Thread類和一些線程同步的類基本的線程知識和多線程編程知識很瞭解。我們將在這裡進一步討論一些.NET類,以及他們在多線程編程中扮演的角色和怎麼編程。它們是:
System.Threading.ThreadPool 類
System.Threading.Timer 類
如果線程的數目並不是很多,而且你想控制每個線程的細節諸如線程的優先順序等,使用Thread是比較合適的;但是如果有大量的線程,考慮使用線程池應該更好一些,它提供了高效的線程管理機制來處理多任務。 對於週期性執行任務Timer類是合適的;使用代表是非同步方法呼叫調用的首選。
System.Threading.ThreadPool Class
當你建立應用程式時,你應該認識到大部分時間你的線程在閒置等待某些事件的發生(諸如按下一個鍵或偵聽套節子的請求)。毫無疑問的,你也會認為這是絕對的浪費資源。
如果這裡有很多的任務需要完成,每個任務需要一個線程,你應該考慮使用線程池來更有效管理你的資源並且從中受益。線程池是執行的多個線程集合,它允許你添加以線程自動建立和開始的任務到隊列裡面去。使用線程池使得你的系統可以最佳化線程在CPU使用時的時間片段。但是要記住在任何特定的時間點,每一個進程和每個線程池只有一個一個正在啟動並執行線程。這個類使得你的線程組成的池可以被系統管理,而使你的主要精力集中在工作流程的邏輯而不是線程的管理。
當第一次執行個體化ThreadPool類時線程池將被建立。它有一個預設的上限,即每處理器最多可以有25個,但是這個上限是可以改變的。這樣使得處理器不會閑置下來。如果其中一個線程等待某個事件的發生,線程池將初始化另外一個線程並投入處理器工作,線程池就是這樣不停的建立工作的線程和分配任務給那些沒有工作的在隊列裡的線程。唯一的限制是背景工作執行緒的數目不能超過最大允許的數目。每個線程將運行在預設的優先順序和使用預設的屬於多線程空間的堆棧大小空間。一旦一項工作任務被排入佇列,你是不能取消的。
請求線程池處理一個任務或者工作項目可以調用QueueUserWorkItem方法。這個方法帶一個WaitCallback代表類型的參數,這個參數封裝了你藥完成的任務。運行時自動為每一個的任務建立線程並且在任務釋放時釋放線程。
下面的代碼說明了如何建立線程池和怎樣新增工作:
public void afunction(object o)
{
// do what ever the function is supposed to do.
}
//thread entry code
{
// create an instance of WaitCallback
WaitCallback myCallback = new WaitCallback (afunction);
//add this to the thread pool / queue a task
ThreadPool.QueueUserWorkItem (myCallback);
}
你也可以通過調用ThreadPool.RegisterWaitForSingleObject方法來傳遞一個System.Threading.WaitHandle,當被通知或者時間超過了調用被System.Threading.WaitOrTimerCallback封裝的方法。
線程池和基於事件的編程模式使得線程池對註冊的WaitHandles的監控和對合適的WaitOrTimerCallback代表方法的調用十分簡單(當WaitHandle被釋放時)。這些做法其實很簡單。這裡有一個線程不斷的觀測線上程池隊列等待操作的狀態。一旦等待操作完成,一個線程將被執行與其對應的任務。因此,這個方法隨著出發觸發事件的發生而增加一個線程。
讓我們看看怎麼隨事件添加一個線程到線程池,其實很簡單。我們只需要建立一個ManualResetEvent類的事件和一個WaitOrTimerCallback的代表,然後我們需要一個攜帶代表狀態的對象,同時我們也要決定休息間隔和執行方式。我們將上面的都添加到線程池,並且激發一個事件:
public void afunction(object o)
{
// do what ever the function is supposed to do.
}
//object that will carry the status info?O:P>
public class anObject
{
}
//thread entry code
{
//create an event object?
ManualResetEvent aevent = new ManualResetEvent (false);
// create an instance of WaitOrTimerCallback
WaitOrTimerCallback thread_method = new WaitOrTimerCallback (afunction);
// create an instance of anObject
anObject myobj = new anObject();
// decide how thread will perform
int timeout_interval = 100; // timeout in milli-seconds.
bool onetime_exec = true;
//add all this to the thread pool.
ThreadPool. RegisterWaitForSingleObject (aevent, thread_method, myobj, timeout_interval, onetime_exec);
// raise the event
aevent.Set();
}
在QueueUserWorkItem和RegisterWaitForSingleObject方法中,線程池建立了一個背景線程來回調。當線程池開始執行一個任務,兩個方法都將調用者的堆棧合并到線程池的線程堆棧中。如果需要安全檢查將耗費更多的時間和增加系統的負擔,因此可以通過使用它們對應的不安全的方法來避免安全檢查。就是ThreadPool.UnsafeRegisterWaitForSingleObject 和ThreadPool.UnsafeQueueUserWorkItem。
你也可以對與等待操作無關的任務排隊。 Timer-queue timers and registered wait operations也使用線程池。它們的返回方法也被放入線程池排隊。
線程池是非常有用的,被廣泛的用於。NET平台上的套節子編程,等待操作註冊,進程計時器和非同步I/O。對於小而短的任務,線程池提供的機制也是十分便利處於多線程的。線程池對於完成許多獨立的任務而且不需要逐個的設定線程屬性是十分便利的。但是,你也應該很清楚,有很多的情況是可以用其他的方法來替代線程池的。比如說你的計劃任務或給每個線程特定的屬性,或者你需要將線程放入單個線程的空間(而線程池是將所有的線程放入一個多線程空間),抑或是一個特定的任務是很冗長的,這些情況你最好考慮清楚,安全的辦法比用線程池應該是你的選擇。
System.Threading.Timer Class
Timer類對於周期性的在分離的線程執行任務是非常有效,它不能被繼承。
這個類尤其用來開發控制台應用程式,因為System.Windows.Forms.Time是停用。比如同來備份檔案和檢查資料庫的一致性。
當建立Timer對象時,你藥估計在第一個代理調用之前等待的時間和後來的每次成功調用之間的時間。一個定時調用發生在方法的應得時間過去,並且在後來周期性的調用這個方法。你可以適應Timer的Change方法來改變這些設定的值或者使Timer失效。當定時器Timer不再使用時,你應該調用Dispose方法來釋放其資源。
TimerCallback代表負責指定與Timer對象相關聯的方法(就是要周期執行的任務)和狀態。它在方法應得的時間過去之後調用一次並且周期性的調用這個方法直到調用了Dispose方法釋放了Timer的所有資源。系統自動分配分離的線程。
讓我們來看一段代碼看看事如何建立Timer對象和使用它的。我們首先要建立一個TimerCallback代理,在後面的方法中要使用到的。如果需要,下一步我們要建立一個狀態物件,它擁有與被代理調用的方法相關聯的特定資訊。為了使這些簡單一些,我們傳遞一個空參數。我們將執行個體化一個Timer對象,然後再使用Change方法改變Timer的設定,最後調用Dispose方法釋放資源。
// class that will be called by the Timer
public class WorkonTimerReq
{
public void aTimerCallMethod()
{
// does some work ?
}
}
//timer creation block
{
//instantiating the class that gets called by the Timer.
WorkonTimerReq anObj = new WorkonTimerReq () ;
// callback delegate
TimerCallback tcallback = new TimerCallback(anObj. aTimerCallMethod) ;
// define the dueTime and period
long dTime = 20 ; // wait before the first tick (in ms)
long pTime = 150 ; // timer during subsequent invocations (in ms)
// instantiate the Timer object
Timer atimer = new Timer(tcallback, null, dTime, pTime) ;
// do some thing with the timer object
//change the dueTime and period of the Timer
dTime=100;
pTime=300;
atimer.Change(dTime, pTime) ;
// do some thing
atimer.Dispose() ;
}
非同步編程
這部分內容如果要講清楚本來就是很大的一部分,在這裡,我不打算詳細討論這個東西,我們只是需要直到它是什麼,因為多線程編程如果忽律非同步多線程編程顯然是不應該的。非同步多線程編程是你的程式可能會用到的另外一種多線程編程方法。
在前面的文章我們花了很大的篇幅來介紹線程的同步和怎麼實現線程的同步,但是它有一個固有的致命的缺點,你或許注意到了這一點。那就是每個線程必須作同步調用,也就是等到其他的功能完成,否則就阻塞。當然,某些情況下,對於那些邏輯上相互依賴的任務來說是足夠的。非同步編程允許更加複雜的靈活性。一個線程可以作非同步呼叫,不需要等待其他的東西。你可以使用這些線程作任何的任務,線程負責擷取結果推進運行。這給予了那些需要管理數目巨大的請求而且負擔不起請求等待代價的企業級的系統更好的延展性。
.NET平台提供了一致的非同步編程機制用於ASP.NET,I/O,Web Services,Networking,Message等。
後記
由於學習的時候很難找到中文這方面的資料,因此我就只好學習英文的資料,由於水平不高,翻譯的時候可能難免曲解原文的意思,希望大家能夠指出,同時希望這些東西能夠給大家在學習這方面知識給予一定的參考和協助,那怕是一點點,就很欣慰了。
毛毛的小窩