簡單的線程實現(xiàn)方式有幾種
l 采用beginInvoke的方式;
l System.Timers.Timer類
l System.Threading.Timer類
l System.Windows.Forms.Timer類
l System.Web.UI.Timer類
后面的三個都可以歸為一個,那就是timer的不同實現(xiàn).其實這個timer的不同實現(xiàn)是針對不同環(huán)境所設(shè)定的.
下面簡單說說Timer類的區(qū)別:
System.Timers.Timer: 它在一個或多個事件接收器中以定期間隔觸發(fā)事件并執(zhí)行相關(guān)的代碼. 該類旨在供使用時為基于服務(wù)器或多線程環(huán)境中的服務(wù)組件,它沒有用戶界面并不是在運行時可見.
System.Threading.Timer:它按定期間隔在線程池線程上執(zhí)行的單個回調(diào)方法. 當(dāng)計時器實例化后不能更改定義的回調(diào)方法.
System.Windows.Forms.Timer以固定的間隔時間觸發(fā)一個或多個事件,執(zhí)行相應(yīng)的代碼的 Windows 窗體組件. 該組件沒有用戶界面,專用于在單線程環(huán)境中;它在 UI 線程上執(zhí)行.
System.Web.UI.Timer: 一種 asp.net 組件,它按定期間隔執(zhí)行異步或同步 web 頁回發(fā).
線程最簡單的實現(xiàn)方式就是創(chuàng)建一個委托然后使用異步的方式調(diào)用.異步委托其實是采用線程池的方式來發(fā)出委托請求.調(diào)用的時候采用BeginInvoke的方法即可.使用起來相當(dāng)簡單.同時委托是線程安全的.
例子如下:
classtestDelgate
{
delegatevoidDoXXOO();
DoXXOO doxxoo =newDoXXOO(() =>
{
Thread.Sleep(2000);
Console.WriteLine("doover ThreadID=" + Thread.CurrentThread.ManagedThreadId);
});
publicvoid TestDo()
{
Console.WriteLine("mainthread ThreadID=" + Thread.CurrentThread.ManagedThreadId);
doxxoo.BeginInvoke(null,null);
Console.WriteLine("mainover");
}
}
可以看出異步委托的執(zhí)行其實是單獨的線程.
不過有一點需要注意:主線程在沒有等到委托線程執(zhí)行完畢就提前結(jié)束,那么委托線程也會結(jié)束.
這個System.timers.Timer這個類是一個定時觸發(fā)事件的一個常用工具.起內(nèi)部是封裝了Threading下面的Timer
以下是Framework中的Timer類的Enabled屬性的源碼(此類的Start方法調(diào)用的是Enabled屬性):
/// <devdoc>/// <para>Gets or sets a value indicating whether the <see cref='System.Timers.Timer'/>/// is able/// to raise events at a defined interval.</para>/// </devdoc>//Microsoft - The default value by design is false, don't change it.[Category("Behavior"), TimersDescription(SR.TimerEnabled), DefaultValue(false)]public bool Enabled {get {return this.enabled;}set {if (DesignMode) {this.delayedEnable = value;this.enabled = value;}else if (initializing)this.delayedEnable = value;else if (enabled != value) {if (!value) {if( timer != null) {cookie = null;timer.Dispose();timer = null;}enabled = value;}else {enabled = value;if( timer == null) {if (disposed) {throw new ObjectDisposedException(GetType().Name);}int i = (int)Math.Ceiling(interval);cookie = new Object();timer = new System.Threading.Timer(callback, cookie, i, autoReset? i:Timeout.Infinite);}else {UpdateTimer();}}}}}
Timer 組件是基于服務(wù)器的計時器,它使您能夠指定在應(yīng)用程序中以周期性的間隔引發(fā) Elapsed 事件. 然后可通過處理這個事件來提供常規(guī)處理.基于服務(wù)器的 Timer 是為在多線程環(huán)境中用于輔助線程而設(shè)計的. 服務(wù)器計時器可以在線程間移動來處理引發(fā)的 Elapsed 事件,這樣就可以比 Windows 計時器更精確地按時引發(fā)事件.Interval 屬性的值可以設(shè)置Timer引發(fā)Elapse的事件的間隔時間. 在Timer中設(shè)置Enabled=true和調(diào)用Start()方法是一樣的.Stop()方法和Enabled=false是一樣的. 只有當(dāng)AutoReset設(shè)置為true的時候,Elapsed事件才會在每次Interval時間間隔到達后引發(fā). 當(dāng) AutoReset 設(shè)置為 false 時,Timer 只在第一個 Interval 過后引發(fā)一次 Elapsed 事件.后面將不再引發(fā)事件.在Elapsed事件中如果要訪問UI需要使用UI線程的invoke的方法.
例子:
classTestTimer
{
PRivate System.Timers.Timer aTimer;
public void Test()
{
aTimer = newSystem.Timers.Timer(10000);
aTimer.Elapsed += newElapsedEventHandler(OnTimedEvent);
aTimer.Interval = 2000;
aTimer.Enabled = true;
Console.WriteLine("start.....");
Console.ReadLine();
GC.KeepAlive(aTimer);
Console.WriteLine("over....");
}
private void OnTimedEvent(object source,ElapsedEventArgs e)
{
Console.WriteLine("TheElapsed event was raised at {0}", e.SignalTime);
}
}
輸出結(jié)果:
2.1.3 Threading.Timer類
此Timer提供用于在指定時間間隔在線程池線程上執(zhí)行一種方法的機制. System.Threading.Timer 是一個簡單, 輕型,它使用回調(diào)方法的計時器.但這個類也是所有timer中內(nèi)部實現(xiàn)最為復(fù)雜的一個.這里就不解釋具體實現(xiàn),有興趣的人可以直接去下載源碼查看(https://referencesource.microsoft.com/#mscorlib/system/threading/timer.cs).
Timer的繼承層次:
由此可以看出繼承層次是很低級別的.
當(dāng)創(chuàng)建一個計時器時,可以指定要在該方法的第一次執(zhí)行之前等待的時間和下次執(zhí)行的間隔時間. Timer 類和系統(tǒng)時鐘頻率一樣.如果間隔時間小于大約15毫秒那么在Windows7/8中將采用系統(tǒng)所定義的時鐘頻率間隔來執(zhí)行TimerCallback 委托.更改到期時間和間隔時間或禁用該計時器,通過使用 Change 方法完成.使用 Dispose方法可以來釋放Timer對象持有的資源.請注意,回調(diào)可能發(fā)生在 Dispose() 已調(diào)用之后,因為計時器是使用線程池的線程來執(zhí)行回調(diào).此時可以使用 Dispose(WaitHandle) 方法重載來等待,直到所有回調(diào)都已都完成.
Timer對象在沒有被引用的時候可能會被垃圾收集器回收.所以在定義的時候請保持對此對象的引用.
例子:
classtestThreadTimer
{
publicvoid test()
{
AutoResetEvent autorest =newAutoResetEvent(false);
int invokeCount = 0;
var t =new System.Threading.Timer((x) =>
{
AutoResetEvent autoWait =(AutoResetEvent)x;
Console.WriteLine("{0} count {1,2}.",
DateTime.Now.ToString("h:mm:ss.fff"),
(++invokeCount).ToString());
if(invokeCount % 10 == 0)
{
autorest.Set();
}
}, autorest, 500, 500);
autorest.WaitOne();
Console.WriteLine("change");
t.Change(1000, 1000);
autorest.WaitOne();
t.Dispose();
Console.WriteLine("over");
}
}
輸出結(jié)果:
從上面也可以看出其實計時不是很精確的.
2.1.4System.Windows.Forms.Timer類
此Timer是Windows 計時器專為使用 UI 線程來執(zhí)行處理的單線程的環(huán)境. 它要求代碼有一個可用的用戶界面消息泵,并且始終在同一個線程操作或到另一個線程的調(diào)用封送.當(dāng)您使用此計時器時,使用 Tick 事件以執(zhí)行輪詢操作也可為指定的時間段顯示一個初始屏幕. 每當(dāng) Enabled 屬性設(shè)置為 true 和 Interval 屬性大于零時, Tick 根據(jù)Interval 屬性設(shè)置的時間間隔引發(fā)事件.
注意:Windows 窗體計時器組件是單線程,并僅限于精度為 55 毫秒.
由于此Timer可以直接拖放到窗體界面然后進行屬性的設(shè)置,使用起來相對簡單不再單獨寫例子.
2.1.5 System.Web.UI.Timer類
Timer 控件使您能夠指定的時間間隔執(zhí)行回發(fā). 當(dāng)您使用 Timer 為觸發(fā)器控制 UpdatePanel 控件, UpdatePanel 控件更新通過使用局部更新. 由于使用了UpdatePanel因此必須包括 ScriptManager 對象在網(wǎng)頁上.
注意現(xiàn)在不建議在網(wǎng)頁上使用此類來做定時操作.建議使用Ajax的方式做局部更新.
2.2 Thread類
使用Thread類創(chuàng)建線程是最常見的方式,但也是最不好控制的方式.Thread類創(chuàng)建的線程模塊不是后臺線程,即: IsBackground屬性為false.這個屬性有什么用呢?當(dāng)主線程結(jié)束時:如果IsBackground屬性為true則線程自動結(jié)束,否則子線程將繼續(xù)運行.
線程有優(yōu)先級,默認情況下線程具有默認優(yōu)先級,由操作系統(tǒng)來負責(zé)調(diào)度和分配.在某些情況下可以單獨設(shè)置線程的優(yōu)先級,以保證當(dāng)前線程能優(yōu)先執(zhí)行.操作系統(tǒng)在調(diào)度線程的時候會根據(jù)優(yōu)先級來選擇優(yōu)先級較高的線程優(yōu)先執(zhí)行.當(dāng)線程不再占有CPU的時候就釋放線程,比如線程在等待磁盤操作結(jié)果,等待網(wǎng)絡(luò)IO結(jié)果等等.
如果線程不是主動釋放CPU,那么線程調(diào)度器就會搶占該線程.如果線程有一個時間量,它就可以繼續(xù)使用CPU.如果優(yōu)先級相同的多個線程等待使用 CPU,線程調(diào)度器就會使用一個循環(huán)調(diào)度規(guī)則,將CPU逐個交給線程使用.如果線程被其他線程搶占,它就會排在隊列的最后:只有優(yōu)先級相同的多個線程在運行,才用得上時間量和循環(huán)規(guī)則.優(yōu)先級是動態(tài)的. 如果線程是運算(CPU)密集型的 (一直需要 CPU,且不等待資源)其優(yōu)先級就低于用該線程定義的基本優(yōu)先級.如果線程在等待資源,隨著優(yōu)先級的提高,它的優(yōu)先級就會增加.由于優(yōu)先級的 提高,線程才有可能在下次等待結(jié)束時獲得CPU.
在Thread類中,可以設(shè)置Priority屬性,以影響線程的基本優(yōu)先級Priority屬性是一個ThreadPriority枚舉定義的一個值. 定義的級別有Lowest,BelowNormal,Normal,AboveNormal,Highest = 4.
線程使用的例子:
classTestThread
{
Thread t1 =null;
Thread t2 =null;
publicvoid CreateThread()
{
Console.WriteLine("Startthread");
t1 = newThread(newThreadStart(() =>
{
Console.WriteLine("i amthread one id=" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000);
Console.WriteLine("i amthread one fuck over");
}));
t1.IsBackground = true;
t2 = newThread(newParameterizedThreadStart((x) =>
{
Console.WriteLine("i amthread tow id=" + Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("i amfuck " + x.ToString());
Thread.Sleep(1000);
Console.WriteLine("i amthread tow fuck over");
}));
t1.Start();
t2.Start("gril");
}
}
輸出結(jié)果:
這里有一點需要注意:線程參數(shù)的傳入,上面的兩個線程初始化的方法是不一樣的.Thread類的構(gòu)函數(shù)有好幾個重載.
public Thread(ThreadStart start);
public Thread(ParameterizedThreadStart start);
這兩個構(gòu)函數(shù)是最常用的.第一個是調(diào)用無參的方法.要求方法返回一個void類型.第二個構(gòu)造函數(shù)需要傳入一個返回void類型的具有一個object類型的參數(shù)的方法.在調(diào)用的時候這個參數(shù)直接通過start的重載傳入.
關(guān)于線程參數(shù),還有一種方式,創(chuàng)建一個類將類中屬性或者其他變量用來保存線程所需參數(shù),在線程執(zhí)行體中直接使用對應(yīng)的變量.
2.3 線程的控制
調(diào)用Thread對 象的Start方法,可以創(chuàng)建線程.但是,在調(diào)用Start()法后,新線程仍不是馬上處于Running狀態(tài),而是處于Unstarted狀態(tài). 直到線程調(diào)度器調(diào)用了該線程,然后才會處于Running狀態(tài).獲取線程狀態(tài)可以用ThreadState屬性來獲取.
使用Thread.Sleep()方法,會使線程處于停止(準確的說是WaitSleepJoin)狀態(tài),在等待Sleep方法指定的時間后線程就會等待再次被喚醒.(不建議使用Sleep放來讓線程處于等待狀態(tài),這種其實沒有釋放CPU,同時也無法做更多的控制.)
要停止另一個線程,可以調(diào)用Abort方法. 調(diào)用這個方法時,會在接到終止命令的線程中拋出一個ThreadAbortException類型的異常. 用一個處理程序捕獲這個異常,線程可以在結(jié)束前完成一些清理工作.如果需要等待線程的結(jié)束,就可以調(diào)用Join方法.Join方法會停止當(dāng)前線程,并把它設(shè)置為WaitSleepJoin狀態(tài),直到加入的線程完成為止.
Abort方法特別說明:
1 當(dāng)在一個線程上調(diào)用此方法時,系統(tǒng)會在其中拋出一個ThreadAbortException這是一個可以由應(yīng)用程序代碼捕獲的特殊異常,但除非調(diào)用 ResetAbort,否則會在 catch 塊的結(jié)尾再次引發(fā)它. ResetAbort 取消中止請求,并防止 ThreadAbortException 終止該線程. 未執(zhí)行的 finally 塊將在線程終止前執(zhí)行.
2 如果正在中止的線程是在受保護的代碼區(qū)域,如 catch 塊、finally 塊或受約束的執(zhí)行區(qū)域,可能會阻止調(diào)用 Abort 的線程. 如果調(diào)用 Abort 的線程持有中止的線程所需的鎖定,則會發(fā)生死鎖.
3如果對尚未啟動的線程調(diào)用 Abort,則當(dāng)調(diào)用 Start 時該線程將中止. 如果對被阻止或正在休眠的線程調(diào)用 Abort,則該線程被中斷然后中止.
4 如果在已掛起的線程上調(diào)用 Abort,則將在調(diào)用 Abort 的線程中引發(fā) ThreadStateException,并將 AbortRequested 添加到被中止的線程的 ThreadState 屬性中. 直到調(diào)用 Resume 后,才在掛起的線程中引發(fā)ThreadAbortException.
5如果在正在執(zhí)行非托管代碼的托管線程上調(diào)用 Abort,則直到線程返回到托管代碼才引發(fā) ThreadAbortException.
6如果同時出現(xiàn)兩次對 Abort 的調(diào)用,則可能一個調(diào)用設(shè)置狀態(tài)信息,而另一個調(diào)用執(zhí)行 Abort. 但是,應(yīng)用程序無法檢測到此情況.
對線程調(diào)用了 Abort 后,線程狀態(tài)包括 AbortRequested. 成功調(diào)用 Abort 而使線程終止后,線程狀態(tài)更改為 Stopped. 如果有足夠的權(quán)限,作為 Abort 目標的線程就可以使用 ResetAbort 方法取消中止操作.
特別說明:
線程中的代碼如果要想保證在線程終止的時候得到執(zhí)行,例如清理一些必要的內(nèi)存等等操作那么必須將這些代碼放到 finally塊中;可以采用如下的寫法:
classtestTry
{
publicvoid test()
{
Thread t =newThread(x =>
{
try
{
}
finally
{
Thread.Sleep(2000);
Console.WriteLine("executethread");
}
});
t.Start();
Thread.Sleep(500);
Console.WriteLine("startabort");
t.Abort();
Console.WriteLine("endabort");
}
}
輸出結(jié)果:
這個代碼沒唯一比較特殊的地方就是try塊中不包括任何邏輯.這樣寫的好處就是當(dāng)線程被終止的時候finally塊中的代碼可以阻止調(diào)用Abort來終止線程直到finally塊執(zhí)行結(jié)束,線程才會終止.
上面的代碼中如果沒有這個try{}finally{},而直接執(zhí)行那么結(jié)果將會如下:
用一個形象的比喻:當(dāng)你作為一個殺手去殺人的時候,被殺的對象正在泡妞,當(dāng)你正要殺他的時候他拿出了一個延遲死的金牌,他的要求等我干完了你在殺.這里殺手就是另一線程,被殺的人就是需要終止的線程,泡妞就是需要終止的線程正在干的事情.延遲死的金牌就是這個try{}finally{}塊.
另外在使用線程的時候不建議使用直接使用Abort來結(jié)束線程.建議讓線程內(nèi)部的代碼執(zhí)行完畢后自然釋放.
2.4 線程池
由于創(chuàng)建線程是一個很耗費資源和時間的操作,因此很有必要減少這種因為創(chuàng)建線程所浪費的資源.線程池ThreadPool類就是這樣的一個工具. 這個類會在需要時增減池中線程的線程數(shù),直到最大的線程數(shù). 池中的最大線程數(shù)是可配置的.通常默認單核CPU最大線程數(shù)量設(shè)置為1023個(不同CPU的內(nèi)核數(shù)量這個結(jié)果不一樣,具體可以使用GetMaxThreads來獲取)工作線程和 1000個I/O線程.也可以指定在創(chuàng)建線程池時應(yīng)立即啟動的最小線程數(shù),以及線程池中可用的最大線程數(shù).如果有更多的任務(wù)要處理,線程池中線程的個數(shù)也到了極限,最新的任務(wù)就要排隊,且必須等待線程完成其任務(wù).如果要自己做線程池一般建議初始化的線程一般為CPU內(nèi)核數(shù)量*2+2.
線程池的使用很簡單:
classTestThreadPool
{
publicvoid ThreadPoolTest()
{
int maxThread, ioMaxThread;
ThreadPool.GetMaxThreads(out maxThread, outioMaxThread);
Console.WriteLine("Workthread max count=" + maxThread + ",IO thread max count =" +ioMaxThread);
for (int i = 0; i < 10; i++)
{
ThreadPool.QueueUserWorkItem(x=>
{
Console.WriteLine("thisis thread pool thread fuck my id=" + Thread.CurrentThread.ManagedThreadId);
});
}
}
}
執(zhí)行結(jié)果:
從上面的輸出中也可以看出有線程已經(jīng)是重用了的.
線程池的使用直接調(diào)用ThreadPool的QueueUserWorkItem方法傳入一個帶有一個object參數(shù)的返回void的方法即可.
線程池的使用有一些限制:
l 線程池中的所有線程都是后臺線程.如果進程的所有前臺線程都結(jié)束了,所有的后臺線程就會停止.不能把入池的線程改為前臺線程.
l 不能給入池的線程設(shè)置優(yōu)先級或名稱.
l 對于COM對象,入池的所有線程都是多線程單元(Multi ThreadedApartments,MTA線程).許多CoM對象都需要單線程單元(Single Threaded Apartment,STA,線程.
l 入池的線程只能用于時間較短的任務(wù). 如果線程要一直運行就應(yīng)使用Thread類創(chuàng)建一個線程.
2.5 Task
Task類的表示單個操作不返回一個值,通常以異步方式執(zhí)行. Task 對象的一個中心思想是基于任務(wù)的異步模式. 因為Task 對象的執(zhí)行工作通常以異步方式在線程池線程上執(zhí)行,而不是以同步方式在主應(yīng)用程序線程上執(zhí)行.可以使用Status屬性,以IsCanceled,IsCompleted,和 IsFaulted 屬性以確定任務(wù)的狀態(tài). 大多數(shù)情況下可以使用lambda 表達式用于指定具體執(zhí)行任務(wù)內(nèi)容.
Task的構(gòu)造函數(shù)有多種重載.可以直接指定任務(wù),但是使用Task類一般不直接調(diào)用構(gòu)造函數(shù)來創(chuàng)建Task對象,而是調(diào)用TaskFactory.StartNew或者Task.Run(注意Run需要4.5以上的Framework才能夠使用,4.0的版本中可以使用Start或者Task.Facorty.StartNew)的來創(chuàng)建.創(chuàng)建完成Task任務(wù)之后其實不是立即執(zhí)行,在內(nèi)部其實有一個隊列排隊調(diào)用線程池的線程來執(zhí)行.如果需要Task返回執(zhí)行結(jié)果那么可以使用Task<TResult>類來完成.
因為任務(wù)通常運行以異步方式在線程池線程上執(zhí)行, 一旦該任務(wù)已實例化,創(chuàng)建并啟動任務(wù)的線程將繼續(xù)執(zhí)行. 在某些情況下,當(dāng)調(diào)用線程的主應(yīng)用程序線程,在實際開始執(zhí)行任務(wù)之前可能會終止任務(wù). 其他情況下,應(yīng)用程序的邏輯可能需要調(diào)用此線程繼續(xù)執(zhí)行直到一個或多個任務(wù)執(zhí)行完畢. 您可以同步調(diào)用線程的執(zhí)行,以及異步任務(wù)它啟動通過調(diào)用 Wait 方法來等待要完成的一個或多個任務(wù).這段話看起來有點拗口,簡單來說就是第一創(chuàng)建Task的線程可能會終止task的任務(wù)同時主創(chuàng)線程可能需要等待task完成來獲取其結(jié)果.第二就是其他線程需要與task來協(xié)同完成一個任務(wù),那么這就需要等待所有線程同時完成.
若要等待完成一項任務(wù),可以調(diào)用其 Task.Wait 方法.調(diào)用 Wait 方法將一直阻塞調(diào)用線程直到單一類實例都已完成執(zhí)行.也可以使用Wait的其他重載,讓Task有條件的等待.
classTestTask
{
publicvoid test()
{
Action<object> action = (object obj) =>
{
Console.WriteLine("Task={0},obj={1}, Thread={2}",
Task.CurrentId,obj,
Thread.CurrentThread.ManagedThreadId);
};
Task t1 =newTask(action,"alpha");
Task t2 =Task.Factory.StartNew(action,"beta");
t2.Wait();
t1.Start();
Console.WriteLine("t1 hasbeen launched. (Main Thread={0})",
Thread.CurrentThread.ManagedThreadId);
t1.Wait();
String taskData ="delta";
Task t3 =Task.Factory.StartNew(() =>
{
Console.WriteLine("Task={0},obj={1}, Thread={2}",
Task.CurrentId,taskData,
Thread.CurrentThread.ManagedThreadId);
});
t3.Wait();
Task t4 =newTask(action,"gamma");
t4.RunSynchronously();
t4.Wait();
}
}
輸出結(jié)果:
Task可以執(zhí)行連續(xù)任務(wù),利用task的ContinueWith()方法.此方法有多個重載.注意在取消任務(wù)的時候連續(xù)任務(wù)也會跟著被取消.
classTestTaskContinue
{
publicvoid Test()
{
Task t =Task.Factory.StartNew(() =>
{
Console.WriteLine("task Astart id=" + Thread.CurrentThread.ManagedThreadId +",TaskID=" +Task.CurrentId);
Thread.Sleep(500);
Console.WriteLine("taskA execute over id=" + Thread.CurrentThread.ManagedThreadId);
});
var t1 = t.ContinueWith(x =>
{
Console.WriteLine("taskt1 start id=" + Thread.CurrentThread.ManagedThreadId +",TaskID=" +Task.CurrentId);
Thread.Sleep(500);
Console.WriteLine("taskt1 execute over id=" + Thread.CurrentThread.ManagedThreadId);
});
var t2 = t.ContinueWith(x =>
{
Console.WriteLine("taskt2 start id=" + Thread.CurrentThread.ManagedThreadId +",TaskID=" +Task.CurrentId);
Thread.Sleep(500);
Console.WriteLine("taskt2 execute over id=" + Thread.CurrentThread.ManagedThreadId);
});
var t3 = t1.ContinueWith(x =>
{
Console.WriteLine("taskt3 start id=" + Thread.CurrentThread.ManagedThreadId +",TaskID=" +Task.CurrentId);
Thread.Sleep(500);
Console.WriteLine("taskt3 execute over id=" + Thread.CurrentThread.ManagedThreadId);
});
var t4 = t3.ContinueWith(x =>
{
Console.WriteLine("taskt4 start id=" + Thread.CurrentThread.ManagedThreadId +",TaskID=" +Task.CurrentId);
Thread.Sleep(500);
Console.WriteLine("taskt4 execute over id=" + Thread.CurrentThread.ManagedThreadId);
});
}
}
輸出結(jié)果:
從這個結(jié)果可以看出其實t1和t2是并行執(zhí)行的,t,t1/t2,t3,t4是串行執(zhí)行的.
2.6 Parallel
此類的主要作用是提供并行循環(huán)運算和區(qū)域性的支持.此類只提供了三個方法For,Foreach和Invoke,這三個方法有多種重載.為集合等提供并行迭代.其內(nèi)部實現(xiàn)是調(diào)用Task來實現(xiàn).因此如果在有順序要求的迭代中不能使用此方法.Invoke方法可以并行執(zhí)行多個方法.
classTestParallel
{
publicvoid Test()
{
List<int> lst = newList<int>();
Parallel.For(0, 10, i =>
{
lst.Add(i);
Console.WriteLine("ThreadID=" + Thread.CurrentThread.ManagedThreadId);
});
Parallel.ForEach(lst, x =>
{
Console.WriteLine("ThreadID=" + Thread.CurrentThread.ManagedThreadId +" Value=" + x);
});
Parallel.Invoke(() =>
{
Console.WriteLine("ThreadID=" + Thread.CurrentThread.ManagedThreadId);
}, () =>
{
Console.WriteLine("ThreadID=" + Thread.CurrentThread.ManagedThreadId);
});
}
}
輸出結(jié)果:
并行任務(wù)在執(zhí)行的過程中其實是可以取消的.在For和foreach的部分重載中有一個ParallelOptions的參數(shù),此參數(shù)可以指定循環(huán).這點和Task的實現(xiàn)是一樣的(內(nèi)部其實就調(diào)用的Task來實現(xiàn)).
實例如下:
publicvoid TestCancel()
{
var cancel =newCancellationTokenSource();
cancel.Token.Register(()=>
{
Console.WriteLine("老子不干了...");
});
Task.Factory.StartNew(() =>
{
Thread.Sleep(100);
cancel.Cancel();
});
try
{
var result =Parallel.For(1,10000,newParallelOptions() { CancellationToken = cancel.Token }, x =>
{
Thread.Sleep(30);
Console.WriteLine("干了" + x + "次" +" thread id =" +Thread.CurrentThread.ManagedThreadId);
});
}catch(Exception e)
{
Console.WriteLine(e.Message);
}
}
輸出結(jié)果:
這里有點需要注意的,就是當(dāng)循環(huán)操作被取消之后會拋出一個”已取消該操作” TaskCanceledException的異常信息.可以直接try..catch來捕獲即可.
2.7 volatile
關(guān)鍵字volatile申明的內(nèi)容是告訴編譯器這些內(nèi)容是給多線程使用的. volatile關(guān)鍵字具有原子特性,所以線程間無法對其占有,它的值永遠是最新的.(以下為MSDN的定義)l volatile 關(guān)鍵字指示一個字段可以由多個同時執(zhí)行的線程修改. 聲明為 volatile 的字段不受編譯器優(yōu)化(假定由單個線程訪問)的限制. 這樣可以確保該字段在任何時間呈現(xiàn)的都是最新的值.l volatile 修飾符通常用于由多個線程訪問但不使用 lock 語句對訪問進行序列化的字段.volatile 關(guān)鍵字可應(yīng)用于以下類型的字段:l 引用類型.l 指針類型(在不安全的上下文中). 請注意,雖然指針本身可以是可變的,但是它指向的對象不能是可變的. 換句話說,您無法聲明“指向可變對象的指針”.l 值類型,如 sbyte、byte、short、ushort、int、uint、char、float 和 bool.l 具有以下基類型之一的枚舉類型:byte、sbyte、short、ushort、int 或 uint.l 已知為引用類型的泛型類型參數(shù).l IntPtr 和 UIntPtr.l 可變關(guān)鍵字僅可應(yīng)用于類或結(jié)構(gòu)字段. 不能將局部變量聲明為 volatile.具體怎么說這個關(guān)鍵字呢,其實很簡單這個關(guān)鍵字的主要用途就是保證多線程的情況下變量數(shù)據(jù)是最新的,因為在獲取某個變量的值的時會先從緩存中獲取,但這在多線程中這個變量的值可能被其他線程修改,但是當(dāng)前線程得不到通知,因此當(dāng)前線程從緩存中獲取的值其實是修改之前的,加了這個關(guān)鍵字之后將不會在從緩存中獲取.2.8 線程同步
2.8.1 Interlocked 鎖
此類為多個線程共享的變量提供原子操作.什么是原子操作呢?舉個簡單的例子,i++這個操作在il代碼中其實還是經(jīng)過了好幾個步驟的.在這個過程中變量的值其實是可能被其他線程改變的.Interlocked可以保證這個操作在完成之前是有效的.
原子操作定義: 如果一個語句執(zhí)行一個單獨不可分割的指令,那么它是原子的.嚴格的原子操作排除了任何搶占的可能性,更方便的理解是這個值永遠是最新的.其實要符合原子操作必須滿足以下條件c#中如果是32位cpu的話,為一個少于等于32位字段賦值是原子操作,其他(自增,讀,寫操作)的則不是.對于64位cpu而言,操作32或64位的字段賦值都屬于原子操作其他讀寫操作都不能屬于原子操作.
此類的方法幫助保護免受計劃程序切換上下文時某個線程正在更新可以由其他線程訪問的變量或者在單獨的處理器上同時執(zhí)行兩個線程就可能出現(xiàn)的錯誤. 此類的成員不會引發(fā)異常.
Increment和 Decrement 方法遞增或遞減的變量并將所得到的值存儲在單個操作. 在大多數(shù)計算機上并遞增一個變量不是一個原子操作,需要執(zhí)行下列步驟:
1 實例變量的值加載到寄存器.
2 遞增或遞減值.
3 將值存儲在實例變量.
如果不使用 Increment 和 Decrement,線程可以優(yōu)先執(zhí)行前兩個步驟之后,另一個線程可以執(zhí)行所有三個步驟. 在第一個線程繼續(xù)執(zhí)行時,它將覆蓋實例變量中的值并且遞增或遞減執(zhí)行的第二個線程的值將丟失.
Exchange方法以原子方式交換指定的變量的值. CompareExchange 方法組合了兩個操作︰ 將兩個值進行比較和存儲第三個值中的某個變量,根據(jù)比較的結(jié)果. 作為一個原子操作執(zhí)行比較和交換操作.
例子如下:
class TestInterlock
{
private static intusingResource = 0;
private const intnumThreadIterations = 5;
private const int numThreads= 10;
public void test()
{
Thread myThread;
Random rnd = newRandom();
for (int i = 0; i <numThreads; i++)
{
myThread = newThread(new ThreadStart(MyThreadProc));
myThread.Name =String.Format("Thread{0}", i + 1);
Thread.Sleep(rnd.Next(0, 1000));
myThread.Start();
}
}
private void MyThreadProc()
{
for (int i = 0; i <numThreadIterations; i++)
{
UseResource();
Thread.Sleep(1000);
}
}
bool UseResource()
{
if (0 ==Interlocked.Exchange(ref usingResource, 1))
{
Console.WriteLine("{0}acquired the lock", Thread.CurrentThread.Name);
Thread.Sleep(500);
Console.WriteLine("{0} exiting lock",Thread.CurrentThread.Name);
Interlocked.Exchange(ref usingResource, 0);
return true;
}
else
{
Console.WriteLine(" {0} wasdenied the lock", Thread.CurrentThread.Name);
return false;
}
}
}
輸出結(jié)果:
從輸出中可以看出其實在對變量usingResource進行賦值操作的時候其實也不是一個原子操作.內(nèi)部還是經(jīng)過了很多步驟,在這個步驟中其他線程是完全有機會去修改這個變量的值,從而導(dǎo)致錯誤.
2.8.2 Lock鎖
Lock語句是在編程的過程中使用較多的一個同步工具類. Lock關(guān)鍵字將語句塊標記為臨界區(qū),方法是獲取給定對象的互斥鎖,執(zhí)行語句,然后釋放該鎖.Lock關(guān)鍵字可以保證被保護的代碼位于零界區(qū)內(nèi),不被其他線程干擾(其他線程無法進入該零界區(qū)).如果其他線程需要進入此零界區(qū)那么必須等待,直到當(dāng)前線程退出零界區(qū).
使用Lock關(guān)鍵字的時候應(yīng)避免一下方式使用:
1 避免使用Lock(this)這樣的方式.
this表示當(dāng)前類的實例,如果鎖定的類是共有類,當(dāng)lock(this)鎖定本身后其他外部類訪問此實例的時候會遇到麻煩.同時this為當(dāng)前對象的實例,如果遇到需要全局獨占對象的鎖定(比如打開串口,文件等等)那么這樣寫是毫無用處的.
2 避免使用lock(typeof(mytype))這類方式的鎖定.
如果鎖定類型mytype是共有類型那么整個類型將會被鎖定.
3 避免使用lock(“aaa”)這類方式的鎖定.
這樣將會導(dǎo)致進程中所有使用同一字符的代碼共享此鎖.
4 鎖定對象必須使用引用類型.
值類型的對象在鎖定的時候有可能是真實對象的副本而不是真實對象,因此無法得到正確的保護.
在使用lock的時候建議使用 private 或者private static的方式定義對象來進行鎖定.
注意在lock語句中無法使用await關(guān)鍵字.
下面是一個經(jīng)常面試遇到的問題例子:
publicvoid test(int i)
{
lock (this)
{
if (i > 10)
{
Console.WriteLine(i);
i--;
test(i);
}
}
Console.WriteLine("Over" + i);
}
這個程序的輸出結(jié)果如下:
這個程序的目的是采用遞歸的方式將傳入的數(shù)據(jù)(當(dāng)前傳入的是18)依次減小到10.而且沒有發(fā)生死鎖,原因很簡單傳入的值是int類型的不是引用類型.另外這里還有一個需要注意的地方,就是在一個線程內(nèi)不論怎么鎖定這個方法都不會死鎖(死鎖是發(fā)生在跨線程共享資源的情況下).
privatestaticobject lck = newobject();
privateint count = 0;
publicvoid test()
{
Thread t1 =newThread(() =>
{
for (int i = 0; i< 5; i++)
add();
});
Thread t2 =newThread(() =>
{
for (int i = 0; i< 5; i++)
add();
});
Thread t3 =newThread(() =>
{
for (int i = 0; i< 5; i++)
add();
});
t1.Start();
t2.Start();
t3.Start();
}
privatevoid add()
{
lock (lck)
{
count++;
Console.WriteLine(count);
}
}
輸出結(jié)果
左邊這個結(jié)果是加鎖的結(jié)果,右邊這個是不加鎖的結(jié)果.而且右邊這個每次運行的結(jié)果都不一樣的.
在使用lock的時候是需要考慮真實情況.對代碼進行加鎖是比較耗費資源和時間的.
在做一個線程安全的類的時候可以采用如下方法:
privateobject olck =newobject();
publicvoid add()
{
lock(olck)
{
Do…..
}
}
不建議在公共類中使用lock(this)來保證線程安全.
Lock語句在編譯的時候會將lock語句編譯成Monitor.Enter 和Monitor.exit的結(jié)構(gòu)(其實就是Monitor的一個簡寫).
Monitor.Enter()
Try{
Do…
}
Finally{
Monitor.exit();
}
2.8.3 Monitor 鎖
Monitor是采用零界區(qū)的方式來提供同步訪問對象的機制. Monitor 類通過向單個線程授予對象鎖來控制對象的訪問. 對象鎖提供限制訪問零界區(qū)的能力. 當(dāng)一個線程擁有對象的鎖時,其他任何線程都不能獲取該鎖. 還可以使用 Monitor 來確保不會允許其他任何線程訪問正在由鎖的所有者執(zhí)行的應(yīng)用程序代碼,除非另一個線程正在使用其他的鎖定對象執(zhí)行該代碼.Monitor鎖定的對象是引用類型而不是值類型(這點和lock一樣,其實lock就是Monitor的簡寫).Monitor是一個靜態(tài)類,無法創(chuàng)建實例.可以直接調(diào)用Monitor的Enter(或者重載)或者TryEnter(或者重載)方法進行加鎖,通過exit的方法釋放鎖.Wait方法可以釋放當(dāng)前線程持有的鎖,讓線程進入等待狀態(tài).
使用Enter 和 Exit 方法標記臨界區(qū)的開頭和結(jié)尾. 如果臨界區(qū)是一個連續(xù)指令集,則由 Enter 方法獲取的鎖將保證只有一個線程可以使用鎖定對象執(zhí)行所包含的代碼. 在這種情況下,將這些指令放在 try塊中,并將 Exit 指令放在 finally 塊中. 此功能通常用于同步對類的靜態(tài)或?qū)嵗椒ǖ脑L問. Enter 和 Exit 方法提供的功能與lock 語句提供的功能相同,區(qū)別在于lock 將 Enter(Object, Boolean) 方法重載和 Exit 方法封裝在 try…finally塊中以確保釋放鎖.
Monitor類有兩組用于 Enter 和 TryEnter 方法的重載. 一組重載具有一個 ref Boolean 參數(shù),在獲取鎖定時自動設(shè)置為 true,即使在獲取鎖定時引發(fā)了異常. 如果釋放在所有實例中的鎖定這點非常重要,即使在該鎖定保護的資源的狀態(tài)可能不一致時,也應(yīng)該使用這些重載.
當(dāng)選擇要同步的對象時,應(yīng)只鎖定私有或內(nèi)部對象. 鎖定外部對象可能導(dǎo)致死鎖,這是因為不相關(guān)的代碼可能會出于不同的目的而選擇鎖定相同的對象.
例子:
classtestMonitor
{
publicvoid test()
{
List<Task> tasks = newList<Task>();
Random rnd =newRandom();
long total = 0;
int n = 0;
for (int taskCtr = 0; taskCtr < 10;taskCtr++)
tasks.Add(Task.Factory.StartNew(()=>
{
int[] values =newint[10000];
int taskTotal =0;
int taskN = 0;
int ctr = 0;
try
{
Monitor.Enter(rnd);
for (ctr = 0;ctr < 10000; ctr++)
values[ctr] = rnd.Next(0, 1001);
}
finally
{
Monitor.Exit(rnd);
}
taskN = ctr;
foreach (var value in values)
taskTotal +=value;
Console.WriteLine("task{0,2}total: {1:N2} (N={2:N0})",
Task.CurrentId,(taskTotal * 1.0) / taskN,
taskN);
Interlocked.Add(ref n, taskN);
Interlocked.Add(ref total,taskTotal);
}));
try
{
Task.WaitAll(tasks.ToArray());
Console.WriteLine("/nalltasks: {0:N2} (N={1:N0})",
(total * 1.0) / n, n);
}
catch (AggregateException e)
{
foreach (var ie ine.InnerExceptions)
Console.WriteLine("{0}:{1}", ie.GetType().Name, ie.Message);
}
}
}
輸出結(jié)果:
2.8.4 SpinLock結(jié)構(gòu)
SpinLock結(jié)構(gòu)通常稱為自旋鎖結(jié)構(gòu). 自旋鎖與互斥鎖有點類似,只是自旋鎖不會引起調(diào)用者睡眠,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持,調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖,"自旋"一詞就是因此而得名.
MSDN定義:自旋鎖提供一個相互排斥鎖的基元,在該基元中,嘗試獲取鎖的線程將在循環(huán)中檢測并等待,直至該鎖變?yōu)榭捎脼橹?
由于自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠高于互斥鎖.如果被保護的共享資源只在進程上下文訪問,使用信號量保護該共享資源非常合適,如果對共享資源的訪問時間非常短,自旋鎖也可以.但是如果被保護的共享資源需要在中斷上下文訪問(包括底半部即中斷處理句柄和頂半部即軟中斷),就必須使用自旋鎖.
自旋鎖保持期間是搶占失效的,而信號量和讀寫信號量保持期間是可以被搶占的.自旋鎖只有在內(nèi)核可搶占或SMP(對稱式多處理器(Symmetric Multi-Processor),縮寫為SMP,是一種計算機系統(tǒng)結(jié)構(gòu))的情況下才真正需要,在單CPU且不可搶占的內(nèi)核下,自旋鎖的所有操作都是空操作.
跟互斥鎖一樣,一個執(zhí)行單元要想訪問被自旋鎖保護的共享資源,必須先得到鎖,在訪問完共享資源后,必須釋放鎖.如果在獲取自旋鎖時,沒有任何執(zhí)行單元保持該鎖,那么將立即得到鎖;如果在獲取自旋鎖時鎖已經(jīng)有保持者,那么獲取鎖操作將自旋在那里,直到該自旋鎖的保持者釋放了鎖.
無論是互斥鎖,還是自旋鎖,在任何時刻,最多只能有一個保持者,也就說,在任何時刻最多只能有一個執(zhí)行單元獲得鎖.
C#中自旋鎖可用于葉級鎖定,此時在大小方面或由于垃圾回收壓力,使用Monitor 所隱含的對象分配消耗過多.自旋鎖非常有助于避免阻塞,但是如果預(yù)期有大量阻塞,由于旋轉(zhuǎn)過多可能不應(yīng)該使用自旋鎖.當(dāng)鎖是細粒度的并且數(shù)量巨大(例如鏈接的列表中每個節(jié)點一個鎖)時以及鎖保持時間總是非常短時,旋轉(zhuǎn)可能非常有幫助.通常,在保持一個旋鎖時,應(yīng)避免任何這些操作:
l 阻塞
l 調(diào)用本身可能阻塞的任何內(nèi)容,
l 一次保持多個自旋鎖,
l 進行動態(tài)調(diào)度的調(diào)用(接口和虛方法)
l 在某一方不擁有的任何代碼中進行動態(tài)調(diào)度的調(diào)用,或分配內(nèi)存.
SpinLock僅當(dāng)您確定這樣做可以改進應(yīng)用程序的性能之后才能使用.另外,務(wù)必請注意 SpinLock 是一個值類型(出于性能原因).因此,您必須非常小心,不要意外復(fù)制了 SpinLock 實例,因為兩個實例(原件和副本)之間完全獨立,這可能會導(dǎo)致應(yīng)用程序出現(xiàn)錯誤行為.如果必須傳遞 SpinLock 實例,則應(yīng)該通過引用而不是通過值傳遞.
不要將SpinLock 實例存儲在只讀字段中.
例子:
public voidtest()
{
SpinLock sl = newSpinLock();
StringBuilder sb = newStringBuilder();
Action action = () =>
{
bool gotLock =false;
for (int i = 0; i< 10000; i++)
{
gotLock = false;
try
{
sl.Enter(refgotLock);
sb.Append((i% 10).ToString());
}
finally
{
if (gotLock)sl.Exit();
}
}
};
Parallel.Invoke(action,action, action);
Console.WriteLine("sb.Length = {0} (should be 30000)",sb.Length);
Console.WriteLine("number of occurrences of '5' in sb: {0} (shouldbe 3000)",
sb.ToString().Where(c => (c == '5')).Count());
}
輸出結(jié)果:
上面這個例子中如果去掉自旋鎖結(jié)果輸出長度可能就不是3000了.(這個例子用lock等也可以實現(xiàn))
SpinLock對于很長時間都不會持有共享資源上的鎖的情況可能很有用. 對于此類情況,在多核計算機上,一種有效的做法是讓已阻塞的線程旋轉(zhuǎn)幾個周期,直至鎖被釋放. 通過旋轉(zhuǎn),線程將不會進入阻塞狀態(tài)(這是一個占用大量 CPU 資源的過程). 在某些情況下,SpinLock將會停止旋轉(zhuǎn),以防止出現(xiàn)邏輯處理器資源不足的現(xiàn)象,或出現(xiàn)系統(tǒng)上超線程的優(yōu)先級反轉(zhuǎn)的情況.
2.8.5 WaitHandle 鎖
WaitHandle是一個抽象基類,用于等待一個信號的設(shè)置. 可以等待不同的信號,因 WaitHandle是一個基類,可以從中派生一些子類.從下圖中也可以看出.
此類有一個重要的屬性:SafeWaitHandle,此屬性可以將一個本機句柄賦予一個操作系統(tǒng)對象.并等待該句柄.比如常見的I/O操作既可以指定一個SafeWaitHandle來等待I/O操作的完成.
2.8.6 Semaphore 鎖
這個鎖屬于信號量類型的鎖,是WaitHandle的子類.他的主要職責(zé)是協(xié)調(diào)各線程之間的關(guān)系,河里使用共享資源.舉個簡單例子,比如有一個餐館里面有五張餐桌,可以同時供5個客人就餐.那么現(xiàn)在來了6個客人,這是老板會讓前面的5個人客人先就餐,然最后到達的客人等待,直到前面的5位客人中有人離去.在這個過程中老板其實就是相當(dāng)于一個信號量,客人其實就是線程,餐桌就是共享資源.
Semaphore信號量是不保證線程進入順序的.線程的喚醒是帶有一定隨機性質(zhì).因此在有順序的要求的請求中請謹慎考慮使用此類.
以下來自MSDN的解釋(英文版自動翻譯的結(jié)果,有些其實自動翻譯是錯誤的):
使用 Semaphore 類來控制對資源池的訪問. 線程進入信號量,通過調(diào)用 WaitOne 方法,繼承自 WaitHandle 類,并通過調(diào)用釋放信號量 Release 方法.上一個信號量計數(shù)會減少在每次一個線程進入信號量,并當(dāng)一個線程釋放信號量時遞增. 當(dāng)該計數(shù)為零時,后續(xù)請求阻止,直到其他線程釋放信號量.如果所有線程都已都釋放信號量,計數(shù)是最大值,是創(chuàng)建信號量的數(shù).重復(fù)調(diào)用 WaitOne 方法可以讓信號量進入多次. 若要釋放部分或所有這些項,線程可以調(diào)用無參數(shù) Release() 多次,也可以調(diào)用的方法重載 Release(Int32) 方法重載來指定要釋放的項數(shù).
Semaphore 類并不強制線程標識在調(diào)用 WaitOne 或 Release. 它是程序員有責(zé)任確保線程不釋放信號量次數(shù)過多. 例如,假定信號量的最大計數(shù)為 2 并且線程 A 和線程 B 都進入了該信號量. 如果線程 B 中的編程錯誤導(dǎo)致它來調(diào)用 Release 兩次,這兩個調(diào)用都成功. 信號量計數(shù)已滿,并且當(dāng)線程A最終調(diào)用Release()將引發(fā)SemaphoreFullException異常.
信號量有兩種類型︰ 本地信號量和已命名的系統(tǒng)信號量. 如果您創(chuàng)建 Semaphore 對象使用的構(gòu)造函數(shù)接受一個名稱,該名稱的操作系統(tǒng)的信號量將與相關(guān)聯(lián). 已命名的系統(tǒng)信號量可以看到在整個操作系統(tǒng),也可用于同步進程間的活動. 您可以創(chuàng)建多個 Semaphore 對象來表示同一個已命名系統(tǒng)信號量,并且你可以使用 OpenExisting 方法以打開一個現(xiàn)有的已命名系統(tǒng)信號量.
您的進程中僅存在了本地信號量. 它可以由具有對本地引用的過程中的任何線程使用 Semaphore 對象. 每個Semaphore 對象是單獨的本地信號量.
Semaphore類有幾個重要方法:
l 構(gòu)造函數(shù)
構(gòu)造函數(shù)中有幾個重載,一般需要指定最大線程數(shù)量和初始化線程數(shù)量.
l WaitOne和重載
表示等待,無參的形式表示無限等待.有參數(shù)表示有條件的等待.
l Release方法
釋放信號量同時返回上一次信號量.注意調(diào)用WaitOne的時候Semaphore會進行計數(shù).Release方法會釋放一次(帶參數(shù)的表示可以釋放參數(shù)指定的次數(shù)).
例子:
classTestSemaphore
{
privateSemaphore _pool;
privateint _padding;
publicvoid Test()
{
_pool = newSemaphore(0, 3);
for (int i = 1; i <= 5; i++)
{
Thread t =newThread(newParameterizedThreadStart(x =>
{
Console.WriteLine("Thread{0} begins and waits for the semaphore.", x);
_pool.WaitOne();
int padding =Interlocked.Add(ref _padding,100);
Console.WriteLine("Thread{0} enters the semaphore.", x);
Thread.Sleep(1000+ padding);
Console.WriteLine("Thread{0} releases the semaphore.", x);
Console.WriteLine("Thread{0} previous semaphore count: {1}",
x,_pool.Release());
}));
t.Start(i);
}
Thread.Sleep(500);
Console.WriteLine("Mainthread calls Release(3).");
_pool.Release(3);
Console.WriteLine("Mainthread exits.");
}
}
輸出結(jié)果:
注意,此示例創(chuàng)建的是默認0個線程可以進入的信號量,因此程序一開始就5個線程就全部等待.然后主線程釋放了3(此處最大為3個)個信號量,這樣其他的線程就可以進入,然后線程執(zhí)行完畢之后,會主動調(diào)用Release方法釋放信號量,其他等待的兩個線程便可進入.
注意:
1 如果Release方法引起 SemaphoreFullException 異常,不一定表示調(diào)用線程有問題.另一個線程中的編程錯誤可能導(dǎo)致該線程退出更多的計數(shù),從而超過它進入的信號量最大值.
2 如果當(dāng)前Semaphore 對象都表示一個已命名的系統(tǒng)信號量(用OpenExiting打開),用戶必須具有 SemaphoreRights.Modify 權(quán)限和信號量必須具有已打開的 SemaphoreRights.Modify 權(quán)限.
2.8.7 mutex 鎖
Mutex類是一個互斥體,可以提供跨進程的同步.mutex和Monitor有點相似,他們都是只能同時有一個線程擁有鎖,能夠訪問同步代碼.mutex可以讓一個帶有名稱的鎖成為進程間的互斥鎖.在服務(wù)器端這里有點不好弄,如果mutex被標記為Global那么具有相同名稱的鎖將在服務(wù)器的所有終端共享,如果被標記為Local那么此鎖僅僅是在服務(wù)端的本次會話中有效.默認情況為Local. Local和Global只針對服務(wù)器會話有效,在進程的作用域中不受影響.
Mutex最常見的用法就是限制進程重復(fù)啟動.例如:
classtestMutex
{
publicvoid test()
{
bool createdNew =false;
Mutex mutex=newMutex(false,"aaaa",outcreatedNew);
if(createdNew)
{
Console.WriteLine("第一次啟動");
}
else
{
Console.WriteLine("第二次啟動");
}
}
}
上述代碼所在的進程啟動兩次結(jié)果如下:
注意:mutex在加鎖某一部分的代碼之后需要明確的調(diào)用ReleaseMutex(),如果沒有釋放互斥快代碼將一直被鎖定.在使用完畢mutex之后需要釋放此類,調(diào)用close方法,或者采用Using的結(jié)構(gòu)讓其自動調(diào)用釋放.建議采用Using的方式.
2.8.8 Event 鎖
注意這個Event不是事件定義的Event關(guān)鍵字,這個Event是只常用的四個用于同步的類. ManualResetEvent, AutoResetEvent, ManualResetEventSlim, CountdownEvent這幾個類.
可以使用事件通知其他任務(wù):這里有一些數(shù)據(jù),并完成了 一些操作等.事件可以發(fā)信號,也可以不發(fā)信號.
2.8.8.1 ManualResetEvent:
調(diào)用 set()方法,等待對象發(fā)喚醒信號.調(diào)用 Reset()方法,可以使之返回不發(fā)信號的狀態(tài).如果多個線程等待向一個事件信號量發(fā)送信號,并調(diào)用了set()方法,就釋放所有等待的線程.另外,如果一個線程剛剛調(diào)用了WaitOne()方法,但事件已經(jīng)發(fā)出信號,等待的線程就可以繼續(xù)等待.
一旦它被終止, ManualResetEvent 手動重置之前一直保持終止狀態(tài). 也就是說,調(diào)用 WaitOne 立即返回.可以控制的初始狀態(tài) ManualResetEvent 通過將一個布爾值傳遞給構(gòu)造函數(shù)中, true 如果初始狀態(tài)終止狀態(tài)和 false 否則為.
2.8.8.2 AutoResetEvent:
線程通過調(diào)用 AutoResetEvent 上的 WaitOne 來等待信號. 如果AutoResetEvent 為未觸發(fā)狀態(tài),則線程會被阻止,并等待當(dāng)前控制資源的線程通過調(diào)用 Set 來通知資源可用.調(diào)用 Set 向 AutoResetEvent 發(fā)信號以釋放等待線程. 當(dāng)AutoResetEvent被設(shè)置為已觸發(fā)狀態(tài)時,它將一直保持已觸發(fā)狀態(tài)直到一個等待的線程被激活,然后它將自動變成未觸發(fā)狀態(tài). 如果沒有任何線程在等待,則狀態(tài)將無限期地保持為已觸發(fā)狀態(tài).當(dāng) AutoResetEvent為已觸發(fā)狀態(tài)時線程調(diào)用 WaitOne,則線程不會被阻止.AutoResetEvent 將立即釋放線程并返回到未觸發(fā)狀態(tài).但是,如果一個線程在等待自動重置的事件發(fā)信號,當(dāng)?shù)谝粋€線程的等待狀態(tài)結(jié)束時,該事件會自動變?yōu)椴话l(fā)信號的狀態(tài).這樣,如果多個線程在等待向事件發(fā)信號,就只有一個線程結(jié)束其等待狀態(tài),它不是等待時間最長的線程,而是優(yōu)先級最高的線程.
注意: Set() 方法不是每次調(diào)用都釋放線程. 如果兩次調(diào)用十分接近,以致在線程釋放之前便已發(fā)生第二次調(diào)用,則只釋放一個線程.就像第二次調(diào)用并未發(fā)生一樣. 另外,如果在調(diào)用 Set 時不存在等待的線程且 AutoResetEvent 已終止,則該調(diào)用無效.也就是說多次調(diào)用set()不會產(chǎn)生副作用.
2.8.8.3 ManualResetEventSlim:
此類其實是ManualResetEvent的一個輕量級版本.在功能和使用上面基本上和ManualResetEvent一樣.但在性能上比ManualResetEvent要好,適合于那些等待時間較短的同步.注意ManualResetEventSlim此類在等待時間較短的情況下采用的是旋轉(zhuǎn)的方式(在較短的等待中旋轉(zhuǎn)比等待句柄開銷要小的多),在等待時間較長的情況下采用的是等待句柄的方式.
2.8.8.4 AutoResetEvent和ManualResetEvent區(qū)別
AutoResetEvent和ManualResetEvent的區(qū)別在于當(dāng)AutoResetEvent調(diào)用Set()之后會自動重置調(diào)用Reset()方法.而ManualResetEvent方法在調(diào)用了Set()之后需要手動調(diào)用Reset()方法否則只要在ManualResetEvent上調(diào)用WaitOne()方法將立即返回.
例子:
classtestAutoEvent
{
AutoResetEvent autoEvent =newAutoResetEvent(false);
List<string> que = newList<string>();
int count = 0;
publicvoid test()
{
Task.Factory.StartNew(() =>
{
while (count <100)
{
if(que.Count() > 0)
{
lock (que)
{
foreach (var xin que)
{
Console.WriteLine("removestring :" + x);
}
que.Clear();
}
count++;
}
else
{
autoEvent.WaitOne();
}
}
});
Task.Factory.StartNew(() =>
{
Random r =newRandom();
for (int i = 0; i< 500; i++)
{
if(r.Next(100) < 50)
{
autoEvent.Set();
}
Thread.Sleep(100);
lock (que)
{
que.Add(i.ToString());
Console.WriteLine("addstring:" + i.ToString());
}
}
});
}
}
結(jié)果:
這個例子有點類似生產(chǎn)者和消費者的例子.
2.8.8.5 CountdownEvent
表示在計數(shù)變?yōu)榱銜r會得到信號通知的同步基元.它在收到一定次數(shù)的信號之后,將會解除對其等待線程的鎖定. CountdownEvent 專門用于以下情況:您必須使用 ManualResetEvent 或 ManualResetEventSlim,并且必須在用信號通知事件之前手動遞減一個變量. 例如,在分叉/聯(lián)接方案中,您可以只創(chuàng)建一個信號計數(shù)為 5 的CountdownEvent,然后在線程池上啟動五個工作項,并且讓每個工作項在完成時調(diào)用 Signal. 每次調(diào)用 Signal 時,信號計數(shù)都會遞減 1. 在主線程上,對 Wait 的調(diào)用將會阻塞,直至信號計數(shù)為零.
例子:
classtestCountdownEvent
{
publicvoid test()
{
CountdownEvent cdevent =newCountdownEvent(4);
Action<object> p = newAction<object>(i =>
{
var c = (int)i;
Thread.Sleep(c *1000);
cdevent.Signal(1);
Console.WriteLine(string.Format("threadid={0} wait {1} over CurrentCount={2}"
, Thread.CurrentThread.ManagedThreadId,c, cdevent.CurrentCount));
});
for (int i = 0; i < 4; i++)
{
Task.Factory.StartNew(p,i + 1);
}
Console.WriteLine("waitall thread over initcount="
+cdevent.InitialCount +",CurrentCount="
+cdevent.CurrentCount +",IsSet="
+ cdevent.IsSet);
cdevent.Wait();
Console.WriteLine("allthread over initcount="
+ cdevent.InitialCount+",CurrentCount="
+cdevent.CurrentCount +",IsSet="
+ cdevent.IsSet);
cdevent.Dispose();
}
}
輸出結(jié)果:
注意此類的Wait(CancellationTokencancellationToken)方法的重載中是可以傳入一個取消等待的對象.
2.8.9 ReaderWriterLock/ReaderWriterLockSlim鎖
這兩個類都是用于定義一個寫和多個讀的加鎖方式.這兩個類都適用于對一個不經(jīng)常發(fā)生變化的資源進行加鎖.在性能上使用這兩個類加鎖一個不經(jīng)常變化的資源比直接使用monitor,mutex等要高出很多.ReaderWriterLockSlim在性能上比ReaderWriterLock要高,他簡化了遞歸,升級和降級的鎖定規(guī)則,同時可以避免潛在的死鎖.因此在實際的項目中建議使用ReaderWriterLockSlim類.下面就只介紹ReaderWriterLockSlim類.
ReaderWriterLockSlim類可以利用EnterReadLock, TryEnterReadLock, EnterWriteLock和TryEnterWriteLock獲取當(dāng)前是讀寫的鎖.還可以使用 EnterUpgradeableReadLock或者TryEnterUpgradeableReadLock函數(shù)類進行先讀后寫的操作(MSDN中把這個叫做升級或者降級,這點比較復(fù)雜不做深入說明,喜歡的朋友可以自己去MSDN看).這個函數(shù)可以讓讀鎖升級,從而不需要釋放讀鎖.注意在調(diào)用了Entry…這類方法之后必須調(diào)用對應(yīng)的exit的方法,例如調(diào)用了EnterReadLock之后需要調(diào)用ExitReadLock;調(diào)用了EnterWriteLock之后需要調(diào)用ExitWriteLock方法來釋放鎖.這個讀寫鎖在讀的情況比寫的情況多很多的情況下比lock性能要高.
例子(這個例子代碼有點多):
classtestReadWriteLockSlim
{
privateList<string> lst = newList<string>();
privateReaderWriterLockSlim lckSlim =newReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
publicvoid test()
{
Task[] t =newTask[6];
t[0] = Task.Factory.StartNew(()=> { Write(); });
t[1] = Task.Factory.StartNew(()=> { Read(); });
t[2] = Task.Factory.StartNew(()=> { Read(); });
t[3] = Task.Factory.StartNew(()=> { Write(); });
t[4] = Task.Factory.StartNew(()=> { Read(); });
t[5] = Task.Factory.StartNew(() => {Read(); });
Task.WaitAll(t);
lckSlim.Dispose();
}
privatevoid Read()
{
try
{
lckSlim.EnterReadLock();
foreach (var x in lst)
{
Console.WriteLine("讀取到:" + x);
}
}
finally
{
lckSlim.ExitReadLock();
}
}
privatevoid Write()
{
try
{
while(!lckSlim.TryEnterWriteLock(-1))
{
Console.WriteLine(string.Format("其他線程(id={0})正在艸,讀就排隊等排隊等待艸的數(shù)量:{1}"
, Thread.CurrentThread.ManagedThreadId,lckSlim.WaitingWriteCount));
Console.WriteLine("等待讀的排隊數(shù)量:" + lckSlim.WaitingReadCount);
}
Console.WriteLine(string.Format("當(dāng)前線程{0}獲取到寫鎖:",Thread.CurrentThread.ManagedThreadId));
for (var r = 0; r< 5; r++)
{
lst.Add("艸你妹 " + r + "次");
Console.WriteLine(string.Format("線程{0}正在操第{1}次",Thread.CurrentThread.ManagedThreadId,r));
Thread.Sleep(50);
}
}
finally
{
Console.WriteLine(string.Format("當(dāng)前線程{0}干完收工:",Thread.CurrentThread.ManagedThreadId));
lckSlim.ExitWriteLock();
}
}
}
輸出結(jié)果:
注意:此類中的從讀模式升級的時候是一個復(fù)雜的過程,我沒搞明白就不在多說.
2.8.10 Barrier 鎖
使多個任務(wù)能夠采用并行方式依據(jù)某種算法在多個階段中協(xié)同工作.這個是什么意思呢,就是說比如abcd四個任務(wù)分別有1,2,3這個三個階段來.每個任務(wù)必須等到其他任務(wù)完成當(dāng)前階段的之后才能夠繼續(xù)下一階段的任務(wù).
例子如下:
classtestBarrier
{
Barrier _Barrier =newBarrier(11);
publicvoid test()
{
Task[] _Tasks =newTask[4];
_Barrier = newBarrier(_Tasks.Count(),(barrier) =>
{
Console.WriteLine("=================Over" + barrier.CurrentPhaseNumber);
});
for (var r = 0; r < _Tasks.Length;r++)
{
_Tasks[r] = Task.Factory.StartNew(()=>
{
DoA();
_Barrier.SignalAndWait();
DoB();
_Barrier.SignalAndWait();
DoC();
_Barrier.SignalAndWait();
});
}
var finalTask =Task.Factory.ContinueWhenAll(_Tasks,(tasks) =>
{
Task.WaitAll(_Tasks);
_Barrier.Dispose();
});
finalTask.Wait();
Console.WriteLine("===============Overall");
}
void DoA()
{
Console.WriteLine("firstA ");
Thread.Sleep(50);
}
void DoB()
{
Console.WriteLine("secondB ");
Thread.Sleep(50);
}
void DoC()
{
Console.WriteLine("threeC ");
Thread.Sleep(50);
}
}
輸出結(jié)果:
注意在調(diào)用Barrier類的時候必須指定參與線程的數(shù)量.
在使用Barrier類的時候有一個稍微不好弄的就是異常處理. 如果進入屏障后,工作的代碼出現(xiàn)了異常,這個異常會被放在BarrierPostPhaseException中,而且所有任務(wù)都能夠捕捉到這個異常.原始的異常可以通過NarrierPostPhaseException 對象的InnerException進行訪問.
2.9 線程總結(jié)
線程在實際的開發(fā)中使用相當(dāng)?shù)亩?在這個過程中使用鎖的時候也非常的多.在選則使用線程的時候可以優(yōu)先考慮使用Task提供功能來完成(微軟Framework自身的代碼中很多多線程的功能都是調(diào)用的Task來完成).其次是直接使用線程池.至于最簡單的Timer其實是最不可靠的(可能是我不太會使用).
線程同步稍微復(fù)雜一點.這個復(fù)雜的表現(xiàn)有幾個方面:1同步的時會影響性能.這點在加鎖的時候就需要考慮在不通的環(huán)境使用不通的鎖.在沒有必要加鎖的地方就不要使用鎖.2 同步中容易發(fā)生死鎖.這個問題一旦發(fā)現(xiàn)而且不容易被發(fā)現(xiàn).有些時候代碼跑幾天都不出現(xiàn)問題.因此查找問題的難度增加.一旦出現(xiàn)這樣的問題請先梳理整個流程.檢查有沒有互相掙鎖的情況.第二可以寫日志來查找.在使用鎖的時候盡量的小粒度加鎖.
文章pdf下載地址:
http://download.csdn.net/detail/shibinysy/9712624
新聞熱點
疑難解答