簡單的線程實現方式有幾種
l 采用beginInvoke的方式;
l System.Timers.Timer類
l System.Threading.Timer類
l System.Windows.Forms.Timer類
l System.Web.UI.Timer類
后面的三個都可以歸為一個,那就是timer的不同實現.其實這個timer的不同實現是針對不同環境所設定的.
下面簡單說說Timer類的區別:
System.Timers.Timer: 它在一個或多個事件接收器中以定期間隔觸發事件并執行相關的代碼. 該類旨在供使用時為基于服務器或多線程環境中的服務組件,它沒有用戶界面并不是在運行時可見.
System.Threading.Timer:它按定期間隔在線程池線程上執行的單個回調方法. 當計時器實例化后不能更改定義的回調方法.
System.Windows.Forms.Timer以固定的間隔時間觸發一個或多個事件,執行相應的代碼的 Windows 窗體組件. 該組件沒有用戶界面,專用于在單線程環境中;它在 UI 線程上執行.
System.Web.UI.Timer: 一種 asp.net 組件,它按定期間隔執行異步或同步 web 頁回發.
線程最簡單的實現方式就是創建一個委托然后使用異步的方式調用.異步委托其實是采用線程池的方式來發出委托請求.調用的時候采用BeginInvoke的方法即可.使用起來相當簡單.同時委托是線程安全的.
例子如下:
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");
}
}
可以看出異步委托的執行其實是單獨的線程.
不過有一點需要注意:主線程在沒有等到委托線程執行完畢就提前結束,那么委托線程也會結束.
這個System.timers.Timer這個類是一個定時觸發事件的一個常用工具.起內部是封裝了Threading下面的Timer
以下是Framework中的Timer類的Enabled屬性的源碼(此類的Start方法調用的是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 組件是基于服務器的計時器,它使您能夠指定在應用程序中以周期性的間隔引發 Elapsed 事件. 然后可通過處理這個事件來提供常規處理.基于服務器的 Timer 是為在多線程環境中用于輔助線程而設計的. 服務器計時器可以在線程間移動來處理引發的 Elapsed 事件,這樣就可以比 Windows 計時器更精確地按時引發事件.Interval 屬性的值可以設置Timer引發Elapse的事件的間隔時間. 在Timer中設置Enabled=true和調用Start()方法是一樣的.Stop()方法和Enabled=false是一樣的. 只有當AutoReset設置為true的時候,Elapsed事件才會在每次Interval時間間隔到達后引發. 當 AutoReset 設置為 false 時,Timer 只在第一個 Interval 過后引發一次 Elapsed 事件.后面將不再引發事件.在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);
}
}
輸出結果:
2.1.3 Threading.Timer類
此Timer提供用于在指定時間間隔在線程池線程上執行一種方法的機制. System.Threading.Timer 是一個簡單, 輕型,它使用回調方法的計時器.但這個類也是所有timer中內部實現最為復雜的一個.這里就不解釋具體實現,有興趣的人可以直接去下載源碼查看(https://referencesource.microsoft.com/#mscorlib/system/threading/timer.cs).
Timer的繼承層次:
由此可以看出繼承層次是很低級別的.
當創建一個計時器時,可以指定要在該方法的第一次執行之前等待的時間和下次執行的間隔時間. Timer 類和系統時鐘頻率一樣.如果間隔時間小于大約15毫秒那么在Windows7/8中將采用系統所定義的時鐘頻率間隔來執行TimerCallback 委托.更改到期時間和間隔時間或禁用該計時器,通過使用 Change 方法完成.使用 Dispose方法可以來釋放Timer對象持有的資源.請注意,回調可能發生在 Dispose() 已調用之后,因為計時器是使用線程池的線程來執行回調.此時可以使用 Dispose(WaitHandle) 方法重載來等待,直到所有回調都已都完成.
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");
}
}
輸出結果:
從上面也可以看出其實計時不是很精確的.
2.1.4System.Windows.Forms.Timer類
此Timer是Windows 計時器專為使用 UI 線程來執行處理的單線程的環境. 它要求代碼有一個可用的用戶界面消息泵,并且始終在同一個線程操作或到另一個線程的調用封送.當您使用此計時器時,使用 Tick 事件以執行輪詢操作也可為指定的時間段顯示一個初始屏幕. 每當 Enabled 屬性設置為 true 和 Interval 屬性大于零時, Tick 根據Interval 屬性設置的時間間隔引發事件.
注意:Windows 窗體計時器組件是單線程,并僅限于精度為 55 毫秒.
由于此Timer可以直接拖放到窗體界面然后進行屬性的設置,使用起來相對簡單不再單獨寫例子.
2.1.5 System.Web.UI.Timer類
Timer 控件使您能夠指定的時間間隔執行回發. 當您使用 Timer 為觸發器控制 UpdatePanel 控件, UpdatePanel 控件更新通過使用局部更新. 由于使用了UpdatePanel因此必須包括 ScriptManager 對象在網頁上.
注意現在不建議在網頁上使用此類來做定時操作.建議使用Ajax的方式做局部更新.
2.2 Thread類
使用Thread類創建線程是最常見的方式,但也是最不好控制的方式.Thread類創建的線程模塊不是后臺線程,即: IsBackground屬性為false.這個屬性有什么用呢?當主線程結束時:如果IsBackground屬性為true則線程自動結束,否則子線程將繼續運行.
線程有優先級,默認情況下線程具有默認優先級,由操作系統來負責調度和分配.在某些情況下可以單獨設置線程的優先級,以保證當前線程能優先執行.操作系統在調度線程的時候會根據優先級來選擇優先級較高的線程優先執行.當線程不再占有CPU的時候就釋放線程,比如線程在等待磁盤操作結果,等待網絡IO結果等等.
如果線程不是主動釋放CPU,那么線程調度器就會搶占該線程.如果線程有一個時間量,它就可以繼續使用CPU.如果優先級相同的多個線程等待使用 CPU,線程調度器就會使用一個循環調度規則,將CPU逐個交給線程使用.如果線程被其他線程搶占,它就會排在隊列的最后:只有優先級相同的多個線程在運行,才用得上時間量和循環規則.優先級是動態的. 如果線程是運算(CPU)密集型的 (一直需要 CPU,且不等待資源)其優先級就低于用該線程定義的基本優先級.如果線程在等待資源,隨著優先級的提高,它的優先級就會增加.由于優先級的 提高,線程才有可能在下次等待結束時獲得CPU.
在Thread類中,可以設置Priority屬性,以影響線程的基本優先級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");
}
}
輸出結果:
這里有一點需要注意:線程參數的傳入,上面的兩個線程初始化的方法是不一樣的.Thread類的構函數有好幾個重載.
public Thread(ThreadStart start);
public Thread(ParameterizedThreadStart start);
這兩個構函數是最常用的.第一個是調用無參的方法.要求方法返回一個void類型.第二個構造函數需要傳入一個返回void類型的具有一個object類型的參數的方法.在調用的時候這個參數直接通過start的重載傳入.
關于線程參數,還有一種方式,創建一個類將類中屬性或者其他變量用來保存線程所需參數,在線程執行體中直接使用對應的變量.
2.3 線程的控制
調用Thread對 象的Start方法,可以創建線程.但是,在調用Start()法后,新線程仍不是馬上處于Running狀態,而是處于Unstarted狀態. 直到線程調度器調用了該線程,然后才會處于Running狀態.獲取線程狀態可以用ThreadState屬性來獲取.
使用Thread.Sleep()方法,會使線程處于停止(準確的說是WaitSleepJoin)狀態,在等待Sleep方法指定的時間后線程就會等待再次被喚醒.(不建議使用Sleep放來讓線程處于等待狀態,這種其實沒有釋放CPU,同時也無法做更多的控制.)
要停止另一個線程,可以調用Abort方法. 調用這個方法時,會在接到終止命令的線程中拋出一個ThreadAbortException類型的異常. 用一個處理程序捕獲這個異常,線程可以在結束前完成一些清理工作.如果需要等待線程的結束,就可以調用Join方法.Join方法會停止當前線程,并把它設置為WaitSleepJoin狀態,直到加入的線程完成為止.
Abort方法特別說明:
1 當在一個線程上調用此方法時,系統會在其中拋出一個ThreadAbortException這是一個可以由應用程序代碼捕獲的特殊異常,但除非調用 ResetAbort,否則會在 catch 塊的結尾再次引發它. ResetAbort 取消中止請求,并防止 ThreadAbortException 終止該線程. 未執行的 finally 塊將在線程終止前執行.
2 如果正在中止的線程是在受保護的代碼區域,如 catch 塊、finally 塊或受約束的執行區域,可能會阻止調用 Abort 的線程. 如果調用 Abort 的線程持有中止的線程所需的鎖定,則會發生死鎖.
3如果對尚未啟動的線程調用 Abort,則當調用 Start 時該線程將中止. 如果對被阻止或正在休眠的線程調用 Abort,則該線程被中斷然后中止.
4 如果在已掛起的線程上調用 Abort,則將在調用 Abort 的線程中引發 ThreadStateException,并將 AbortRequested 添加到被中止的線程的 ThreadState 屬性中. 直到調用 Resume 后,才在掛起的線程中引發ThreadAbortException.
5如果在正在執行非托管代碼的托管線程上調用 Abort,則直到線程返回到托管代碼才引發 ThreadAbortException.
6如果同時出現兩次對 Abort 的調用,則可能一個調用設置狀態信息,而另一個調用執行 Abort. 但是,應用程序無法檢測到此情況.
對線程調用了 Abort 后,線程狀態包括 AbortRequested. 成功調用 Abort 而使線程終止后,線程狀態更改為 Stopped. 如果有足夠的權限,作為 Abort 目標的線程就可以使用 ResetAbort 方法取消中止操作.
特別說明:
線程中的代碼如果要想保證在線程終止的時候得到執行,例如清理一些必要的內存等等操作那么必須將這些代碼放到 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");
}
}
輸出結果:
這個代碼沒唯一比較特殊的地方就是try塊中不包括任何邏輯.這樣寫的好處就是當線程被終止的時候finally塊中的代碼可以阻止調用Abort來終止線程直到finally塊執行結束,線程才會終止.
上面的代碼中如果沒有這個try{}finally{},而直接執行那么結果將會如下:
用一個形象的比喻:當你作為一個殺手去殺人的時候,被殺的對象正在泡妞,當你正要殺他的時候他拿出了一個延遲死的金牌,他的要求等我干完了你在殺.這里殺手就是另一線程,被殺的人就是需要終止的線程,泡妞就是需要終止的線程正在干的事情.延遲死的金牌就是這個try{}finally{}塊.
另外在使用線程的時候不建議使用直接使用Abort來結束線程.建議讓線程內部的代碼執行完畢后自然釋放.
2.4 線程池
由于創建線程是一個很耗費資源和時間的操作,因此很有必要減少這種因為創建線程所浪費的資源.線程池ThreadPool類就是這樣的一個工具. 這個類會在需要時增減池中線程的線程數,直到最大的線程數. 池中的最大線程數是可配置的.通常默認單核CPU最大線程數量設置為1023個(不同CPU的內核數量這個結果不一樣,具體可以使用GetMaxThreads來獲取)工作線程和 1000個I/O線程.也可以指定在創建線程池時應立即啟動的最小線程數,以及線程池中可用的最大線程數.如果有更多的任務要處理,線程池中線程的個數也到了極限,最新的任務就要排隊,且必須等待線程完成其任務.如果要自己做線程池一般建議初始化的線程一般為CPU內核數量*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);
});
}
}
}
執行結果:
從上面的輸出中也可以看出有線程已經是重用了的.
線程池的使用直接調用ThreadPool的QueueUserWorkItem方法傳入一個帶有一個object參數的返回void的方法即可.
線程池的使用有一些限制:
l 線程池中的所有線程都是后臺線程.如果進程的所有前臺線程都結束了,所有的后臺線程就會停止.不能把入池的線程改為前臺線程.
l 不能給入池的線程設置優先級或名稱.
l 對于COM對象,入池的所有線程都是多線程單元(Multi ThreadedApartments,MTA線程).許多CoM對象都需要單線程單元(Single Threaded Apartment,STA,線程.
l 入池的線程只能用于時間較短的任務. 如果線程要一直運行就應使用Thread類創建一個線程.
2.5 Task
Task類的表示單個操作不返回一個值,通常以異步方式執行. Task 對象的一個中心思想是基于任務的異步模式. 因為Task 對象的執行工作通常以異步方式在線程池線程上執行,而不是以同步方式在主應用程序線程上執行.可以使用Status屬性,以IsCanceled,IsCompleted,和 IsFaulted 屬性以確定任務的狀態. 大多數情況下可以使用lambda 表達式用于指定具體執行任務內容.
Task的構造函數有多種重載.可以直接指定任務,但是使用Task類一般不直接調用構造函數來創建Task對象,而是調用TaskFactory.StartNew或者Task.Run(注意Run需要4.5以上的Framework才能夠使用,4.0的版本中可以使用Start或者Task.Facorty.StartNew)的來創建.創建完成Task任務之后其實不是立即執行,在內部其實有一個隊列排隊調用線程池的線程來執行.如果需要Task返回執行結果那么可以使用Task<TResult>類來完成.
因為任務通常運行以異步方式在線程池線程上執行, 一旦該任務已實例化,創建并啟動任務的線程將繼續執行. 在某些情況下,當調用線程的主應用程序線程,在實際開始執行任務之前可能會終止任務. 其他情況下,應用程序的邏輯可能需要調用此線程繼續執行直到一個或多個任務執行完畢. 您可以同步調用線程的執行,以及異步任務它啟動通過調用 Wait 方法來等待要完成的一個或多個任務.這段話看起來有點拗口,簡單來說就是第一創建Task的線程可能會終止task的任務同時主創線程可能需要等待task完成來獲取其結果.第二就是其他線程需要與task來協同完成一個任務,那么這就需要等待所有線程同時完成.
若要等待完成一項任務,可以調用其 Task.Wait 方法.調用 Wait 方法將一直阻塞調用線程直到單一類實例都已完成執行.也可以使用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();
}
}
輸出結果:
Task可以執行連續任務,利用task的ContinueWith()方法.此方法有多個重載.注意在取消任務的時候連續任務也會跟著被取消.
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);
});
}
}
輸出結果:
從這個結果可以看出其實t1和t2是并行執行的,t,t1/t2,t3,t4是串行執行的.
2.6 Parallel
此類的主要作用是提供并行循環運算和區域性的支持.此類只提供了三個方法For,Foreach和Invoke,這三個方法有多種重載.為集合等提供并行迭代.其內部實現是調用Task來實現.因此如果在有順序要求的迭代中不能使用此方法.Invoke方法可以并行執行多個方法.
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);
});
}
}
輸出結果:
并行任務在執行的過程中其實是可以取消的.在For和foreach的部分重載中有一個ParallelOptions的參數,此參數可以指定循環.這點和Task的實現是一樣的(內部其實就調用的Task來實現).
實例如下:
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);
}
}
輸出結果:
這里有點需要注意的,就是當循環操作被取消之后會拋出一個”已取消該操作” TaskCanceledException的異常信息.可以直接try..catch來捕獲即可.
2.7 volatile
關鍵字volatile申明的內容是告訴編譯器這些內容是給多線程使用的. volatile關鍵字具有原子特性,所以線程間無法對其占有,它的值永遠是最新的.(以下為MSDN的定義)l volatile 關鍵字指示一個字段可以由多個同時執行的線程修改. 聲明為 volatile 的字段不受編譯器優化(假定由單個線程訪問)的限制. 這樣可以確保該字段在任何時間呈現的都是最新的值.l volatile 修飾符通常用于由多個線程訪問但不使用 lock 語句對訪問進行序列化的字段.volatile 關鍵字可應用于以下類型的字段:l 引用類型.l 指針類型(在不安全的上下文中). 請注意,雖然指針本身可以是可變的,但是它指向的對象不能是可變的. 換句話說,您無法聲明“指向可變對象的指針”.l 值類型,如 sbyte、byte、short、ushort、int、uint、char、float 和 bool.l 具有以下基類型之一的枚舉類型:byte、sbyte、short、ushort、int 或 uint.l 已知為引用類型的泛型類型參數.l IntPtr 和 UIntPtr.l 可變關鍵字僅可應用于類或結構字段. 不能將局部變量聲明為 volatile.具體怎么說這個關鍵字呢,其實很簡單這個關鍵字的主要用途就是保證多線程的情況下變量數據是最新的,因為在獲取某個變量的值的時會先從緩存中獲取,但這在多線程中這個變量的值可能被其他線程修改,但是當前線程得不到通知,因此當前線程從緩存中獲取的值其實是修改之前的,加了這個關鍵字之后將不會在從緩存中獲取.2.8 線程同步
2.8.1 Interlocked 鎖
此類為多個線程共享的變量提供原子操作.什么是原子操作呢?舉個簡單的例子,i++這個操作在il代碼中其實還是經過了好幾個步驟的.在這個過程中變量的值其實是可能被其他線程改變的.Interlocked可以保證這個操作在完成之前是有效的.
原子操作定義: 如果一個語句執行一個單獨不可分割的指令,那么它是原子的.嚴格的原子操作排除了任何搶占的可能性,更方便的理解是這個值永遠是最新的.其實要符合原子操作必須滿足以下條件c#中如果是32位cpu的話,為一個少于等于32位字段賦值是原子操作,其他(自增,讀,寫操作)的則不是.對于64位cpu而言,操作32或64位的字段賦值都屬于原子操作其他讀寫操作都不能屬于原子操作.
此類的方法幫助保護免受計劃程序切換上下文時某個線程正在更新可以由其他線程訪問的變量或者在單獨的處理器上同時執行兩個線程就可能出現的錯誤. 此類的成員不會引發異常.
Increment和 Decrement 方法遞增或遞減的變量并將所得到的值存儲在單個操作. 在大多數計算機上并遞增一個變量不是一個原子操作,需要執行下列步驟:
1 實例變量的值加載到寄存器.
2 遞增或遞減值.
3 將值存儲在實例變量.
如果不使用 Increment 和 Decrement,線程可以優先執行前兩個步驟之后,另一個線程可以執行所有三個步驟. 在第一個線程繼續執行時,它將覆蓋實例變量中的值并且遞增或遞減執行的第二個線程的值將丟失.
Exchange方法以原子方式交換指定的變量的值. CompareExchange 方法組合了兩個操作︰ 將兩個值進行比較和存儲第三個值中的某個變量,根據比較的結果. 作為一個原子操作執行比較和交換操作.
例子如下:
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;
}
}
}
輸出結果:
從輸出中可以看出其實在對變量usingResource進行賦值操作的時候其實也不是一個原子操作.內部還是經過了很多步驟,在這個步驟中其他線程是完全有機會去修改這個變量的值,從而導致錯誤.
2.8.2 Lock鎖
Lock語句是在編程的過程中使用較多的一個同步工具類. Lock關鍵字將語句塊標記為臨界區,方法是獲取給定對象的互斥鎖,執行語句,然后釋放該鎖.Lock關鍵字可以保證被保護的代碼位于零界區內,不被其他線程干擾(其他線程無法進入該零界區).如果其他線程需要進入此零界區那么必須等待,直到當前線程退出零界區.
使用Lock關鍵字的時候應避免一下方式使用:
1 避免使用Lock(this)這樣的方式.
this表示當前類的實例,如果鎖定的類是共有類,當lock(this)鎖定本身后其他外部類訪問此實例的時候會遇到麻煩.同時this為當前對象的實例,如果遇到需要全局獨占對象的鎖定(比如打開串口,文件等等)那么這樣寫是毫無用處的.
2 避免使用lock(typeof(mytype))這類方式的鎖定.
如果鎖定類型mytype是共有類型那么整個類型將會被鎖定.
3 避免使用lock(“aaa”)這類方式的鎖定.
這樣將會導致進程中所有使用同一字符的代碼共享此鎖.
4 鎖定對象必須使用引用類型.
值類型的對象在鎖定的時候有可能是真實對象的副本而不是真實對象,因此無法得到正確的保護.
在使用lock的時候建議使用 private 或者private static的方式定義對象來進行鎖定.
注意在lock語句中無法使用await關鍵字.
下面是一個經常面試遇到的問題例子:
publicvoid test(int i)
{
lock (this)
{
if (i > 10)
{
Console.WriteLine(i);
i--;
test(i);
}
}
Console.WriteLine("Over" + i);
}
這個程序的輸出結果如下:
這個程序的目的是采用遞歸的方式將傳入的數據(當前傳入的是18)依次減小到10.而且沒有發生死鎖,原因很簡單傳入的值是int類型的不是引用類型.另外這里還有一個需要注意的地方,就是在一個線程內不論怎么鎖定這個方法都不會死鎖(死鎖是發生在跨線程共享資源的情況下).
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);
}
}
輸出結果
左邊這個結果是加鎖的結果,右邊這個是不加鎖的結果.而且右邊這個每次運行的結果都不一樣的.
在使用lock的時候是需要考慮真實情況.對代碼進行加鎖是比較耗費資源和時間的.
在做一個線程安全的類的時候可以采用如下方法:
privateobject olck =newobject();
publicvoid add()
{
lock(olck)
{
Do…..
}
}
不建議在公共類中使用lock(this)來保證線程安全.
Lock語句在編譯的時候會將lock語句編譯成Monitor.Enter 和Monitor.exit的結構(其實就是Monitor的一個簡寫).
Monitor.Enter()
Try{
Do…
}
Finally{
Monitor.exit();
}
2.8.3 Monitor 鎖
Monitor是采用零界區的方式來提供同步訪問對象的機制. Monitor 類通過向單個線程授予對象鎖來控制對象的訪問. 對象鎖提供限制訪問零界區的能力. 當一個線程擁有對象的鎖時,其他任何線程都不能獲取該鎖. 還可以使用 Monitor 來確保不會允許其他任何線程訪問正在由鎖的所有者執行的應用程序代碼,除非另一個線程正在使用其他的鎖定對象執行該代碼.Monitor鎖定的對象是引用類型而不是值類型(這點和lock一樣,其實lock就是Monitor的簡寫).Monitor是一個靜態類,無法創建實例.可以直接調用Monitor的Enter(或者重載)或者TryEnter(或者重載)方法進行加鎖,通過exit的方法釋放鎖.Wait方法可以釋放當前線程持有的鎖,讓線程進入等待狀態.
使用Enter 和 Exit 方法標記臨界區的開頭和結尾. 如果臨界區是一個連續指令集,則由 Enter 方法獲取的鎖將保證只有一個線程可以使用鎖定對象執行所包含的代碼. 在這種情況下,將這些指令放在 try塊中,并將 Exit 指令放在 finally 塊中. 此功能通常用于同步對類的靜態或實例方法的訪問. Enter 和 Exit 方法提供的功能與lock 語句提供的功能相同,區別在于lock 將 Enter(Object, Boolean) 方法重載和 Exit 方法封裝在 try…finally塊中以確保釋放鎖.
Monitor類有兩組用于 Enter 和 TryEnter 方法的重載. 一組重載具有一個 ref Boolean 參數,在獲取鎖定時自動設置為 true,即使在獲取鎖定時引發了異常. 如果釋放在所有實例中的鎖定這點非常重要,即使在該鎖定保護的資源的狀態可能不一致時,也應該使用這些重載.
當選擇要同步的對象時,應只鎖定私有或內部對象. 鎖定外部對象可能導致死鎖,這是因為不相關的代碼可能會出于不同的目的而選擇鎖定相同的對象.
例子:
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);
}
}
}
輸出結果:
2.8.4 SpinLock結構
SpinLock結構通常稱為自旋鎖結構. 自旋鎖與互斥鎖有點類似,只是自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環在那里看是否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是因此而得名.
MSDN定義:自旋鎖提供一個相互排斥鎖的基元,在該基元中,嘗試獲取鎖的線程將在循環中檢測并等待,直至該鎖變為可用為止.
由于自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠高于互斥鎖.如果被保護的共享資源只在進程上下文訪問,使用信號量保護該共享資源非常合適,如果對共享資源的訪問時間非常短,自旋鎖也可以.但是如果被保護的共享資源需要在中斷上下文訪問(包括底半部即中斷處理句柄和頂半部即軟中斷),就必須使用自旋鎖.
自旋鎖保持期間是搶占失效的,而信號量和讀寫信號量保持期間是可以被搶占的.自旋鎖只有在內核可搶占或SMP(對稱式多處理器(Symmetric Multi-Processor),縮寫為SMP,是一種計算機系統結構)的情況下才真正需要,在單CPU且不可搶占的內核下,自旋鎖的所有操作都是空操作.
跟互斥鎖一樣,一個執行單元要想訪問被自旋鎖保護的共享資源,必須先得到鎖,在訪問完共享資源后,必須釋放鎖.如果在獲取自旋鎖時,沒有任何執行單元保持該鎖,那么將立即得到鎖;如果在獲取自旋鎖時鎖已經有保持者,那么獲取鎖操作將自旋在那里,直到該自旋鎖的保持者釋放了鎖.
無論是互斥鎖,還是自旋鎖,在任何時刻,最多只能有一個保持者,也就說,在任何時刻最多只能有一個執行單元獲得鎖.
C#中自旋鎖可用于葉級鎖定,此時在大小方面或由于垃圾回收壓力,使用Monitor 所隱含的對象分配消耗過多.自旋鎖非常有助于避免阻塞,但是如果預期有大量阻塞,由于旋轉過多可能不應該使用自旋鎖.當鎖是細粒度的并且數量巨大(例如鏈接的列表中每個節點一個鎖)時以及鎖保持時間總是非常短時,旋轉可能非常有幫助.通常,在保持一個旋鎖時,應避免任何這些操作:
l 阻塞
l 調用本身可能阻塞的任何內容,
l 一次保持多個自旋鎖,
l 進行動態調度的調用(接口和虛方法)
l 在某一方不擁有的任何代碼中進行動態調度的調用,或分配內存.
SpinLock僅當您確定這樣做可以改進應用程序的性能之后才能使用.另外,務必請注意 SpinLock 是一個值類型(出于性能原因).因此,您必須非常小心,不要意外復制了 SpinLock 實例,因為兩個實例(原件和副本)之間完全獨立,這可能會導致應用程序出現錯誤行為.如果必須傳遞 SpinLock 實例,則應該通過引用而不是通過值傳遞.
不要將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());
}
輸出結果:
上面這個例子中如果去掉自旋鎖結果輸出長度可能就不是3000了.(這個例子用lock等也可以實現)
SpinLock對于很長時間都不會持有共享資源上的鎖的情況可能很有用. 對于此類情況,在多核計算機上,一種有效的做法是讓已阻塞的線程旋轉幾個周期,直至鎖被釋放. 通過旋轉,線程將不會進入阻塞狀態(這是一個占用大量 CPU 資源的過程). 在某些情況下,SpinLock將會停止旋轉,以防止出現邏輯處理器資源不足的現象,或出現系統上超線程的優先級反轉的情況.
2.8.5 WaitHandle 鎖
WaitHandle是一個抽象基類,用于等待一個信號的設置. 可以等待不同的信號,因 WaitHandle是一個基類,可以從中派生一些子類.從下圖中也可以看出.
此類有一個重要的屬性:SafeWaitHandle,此屬性可以將一個本機句柄賦予一個操作系統對象.并等待該句柄.比如常見的I/O操作既可以指定一個SafeWaitHandle來等待I/O操作的完成.
2.8.6 Semaphore 鎖
這個鎖屬于信號量類型的鎖,是WaitHandle的子類.他的主要職責是協調各線程之間的關系,河里使用共享資源.舉個簡單例子,比如有一個餐館里面有五張餐桌,可以同時供5個客人就餐.那么現在來了6個客人,這是老板會讓前面的5個人客人先就餐,然最后到達的客人等待,直到前面的5位客人中有人離去.在這個過程中老板其實就是相當于一個信號量,客人其實就是線程,餐桌就是共享資源.
Semaphore信號量是不保證線程進入順序的.線程的喚醒是帶有一定隨機性質.因此在有順序的要求的請求中請謹慎考慮使用此類.
以下來自MSDN的解釋(英文版自動翻譯的結果,有些其實自動翻譯是錯誤的):
使用 Semaphore 類來控制對資源池的訪問. 線程進入信號量,通過調用 WaitOne 方法,繼承自 WaitHandle 類,并通過調用釋放信號量 Release 方法.上一個信號量計數會減少在每次一個線程進入信號量,并當一個線程釋放信號量時遞增. 當該計數為零時,后續請求阻止,直到其他線程釋放信號量.如果所有線程都已都釋放信號量,計數是最大值,是創建信號量的數.重復調用 WaitOne 方法可以讓信號量進入多次. 若要釋放部分或所有這些項,線程可以調用無參數 Release() 多次,也可以調用的方法重載 Release(Int32) 方法重載來指定要釋放的項數.
Semaphore 類并不強制線程標識在調用 WaitOne 或 Release. 它是程序員有責任確保線程不釋放信號量次數過多. 例如,假定信號量的最大計數為 2 并且線程 A 和線程 B 都進入了該信號量. 如果線程 B 中的編程錯誤導致它來調用 Release 兩次,這兩個調用都成功. 信號量計數已滿,并且當線程A最終調用Release()將引發SemaphoreFullException異常.
信號量有兩種類型︰ 本地信號量和已命名的系統信號量. 如果您創建 Semaphore 對象使用的構造函數接受一個名稱,該名稱的操作系統的信號量將與相關聯. 已命名的系統信號量可以看到在整個操作系統,也可用于同步進程間的活動. 您可以創建多個 Semaphore 對象來表示同一個已命名系統信號量,并且你可以使用 OpenExisting 方法以打開一個現有的已命名系統信號量.
您的進程中僅存在了本地信號量. 它可以由具有對本地引用的過程中的任何線程使用 Semaphore 對象. 每個Semaphore 對象是單獨的本地信號量.
Semaphore類有幾個重要方法:
l 構造函數
構造函數中有幾個重載,一般需要指定最大線程數量和初始化線程數量.
l WaitOne和重載
表示等待,無參的形式表示無限等待.有參數表示有條件的等待.
l Release方法
釋放信號量同時返回上一次信號量.注意調用WaitOne的時候Semaphore會進行計數.Release方法會釋放一次(帶參數的表示可以釋放參數指定的次數).
例子:
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.");
}
}
輸出結果:
注意,此示例創建的是默認0個線程可以進入的信號量,因此程序一開始就5個線程就全部等待.然后主線程釋放了3(此處最大為3個)個信號量,這樣其他的線程就可以進入,然后線程執行完畢之后,會主動調用Release方法釋放信號量,其他等待的兩個線程便可進入.
注意:
1 如果Release方法引起 SemaphoreFullException 異常,不一定表示調用線程有問題.另一個線程中的編程錯誤可能導致該線程退出更多的計數,從而超過它進入的信號量最大值.
2 如果當前Semaphore 對象都表示一個已命名的系統信號量(用OpenExiting打開),用戶必須具有 SemaphoreRights.Modify 權限和信號量必須具有已打開的 SemaphoreRights.Modify 權限.
2.8.7 mutex 鎖
Mutex類是一個互斥體,可以提供跨進程的同步.mutex和Monitor有點相似,他們都是只能同時有一個線程擁有鎖,能夠訪問同步代碼.mutex可以讓一個帶有名稱的鎖成為進程間的互斥鎖.在服務器端這里有點不好弄,如果mutex被標記為Global那么具有相同名稱的鎖將在服務器的所有終端共享,如果被標記為Local那么此鎖僅僅是在服務端的本次會話中有效.默認情況為Local. Local和Global只針對服務器會話有效,在進程的作用域中不受影響.
Mutex最常見的用法就是限制進程重復啟動.例如:
classtestMutex
{
publicvoid test()
{
bool createdNew =false;
Mutex mutex=newMutex(false,"aaaa",outcreatedNew);
if(createdNew)
{
Console.WriteLine("第一次啟動");
}
else
{
Console.WriteLine("第二次啟動");
}
}
}
上述代碼所在的進程啟動兩次結果如下:
注意:mutex在加鎖某一部分的代碼之后需要明確的調用ReleaseMutex(),如果沒有釋放互斥快代碼將一直被鎖定.在使用完畢mutex之后需要釋放此類,調用close方法,或者采用Using的結構讓其自動調用釋放.建議采用Using的方式.
2.8.8 Event 鎖
注意這個Event不是事件定義的Event關鍵字,這個Event是只常用的四個用于同步的類. ManualResetEvent, AutoResetEvent, ManualResetEventSlim, CountdownEvent這幾個類.
可以使用事件通知其他任務:這里有一些數據,并完成了 一些操作等.事件可以發信號,也可以不發信號.
2.8.8.1 ManualResetEvent:
調用 set()方法,等待對象發喚醒信號.調用 Reset()方法,可以使之返回不發信號的狀態.如果多個線程等待向一個事件信號量發送信號,并調用了set()方法,就釋放所有等待的線程.另外,如果一個線程剛剛調用了WaitOne()方法,但事件已經發出信號,等待的線程就可以繼續等待.
一旦它被終止, ManualResetEvent 手動重置之前一直保持終止狀態. 也就是說,調用 WaitOne 立即返回.可以控制的初始狀態 ManualResetEvent 通過將一個布爾值傳遞給構造函數中, true 如果初始狀態終止狀態和 false 否則為.
2.8.8.2 AutoResetEvent:
線程通過調用 AutoResetEvent 上的 WaitOne 來等待信號. 如果AutoResetEvent 為未觸發狀態,則線程會被阻止,并等待當前控制資源的線程通過調用 Set 來通知資源可用.調用 Set 向 AutoResetEvent 發信號以釋放等待線程. 當AutoResetEvent被設置為已觸發狀態時,它將一直保持已觸發狀態直到一個等待的線程被激活,然后它將自動變成未觸發狀態. 如果沒有任何線程在等待,則狀態將無限期地保持為已觸發狀態.當 AutoResetEvent為已觸發狀態時線程調用 WaitOne,則線程不會被阻止.AutoResetEvent 將立即釋放線程并返回到未觸發狀態.但是,如果一個線程在等待自動重置的事件發信號,當第一個線程的等待狀態結束時,該事件會自動變為不發信號的狀態.這樣,如果多個線程在等待向事件發信號,就只有一個線程結束其等待狀態,它不是等待時間最長的線程,而是優先級最高的線程.
注意: Set() 方法不是每次調用都釋放線程. 如果兩次調用十分接近,以致在線程釋放之前便已發生第二次調用,則只釋放一個線程.就像第二次調用并未發生一樣. 另外,如果在調用 Set 時不存在等待的線程且 AutoResetEvent 已終止,則該調用無效.也就是說多次調用set()不會產生副作用.
2.8.8.3 ManualResetEventSlim:
此類其實是ManualResetEvent的一個輕量級版本.在功能和使用上面基本上和ManualResetEvent一樣.但在性能上比ManualResetEvent要好,適合于那些等待時間較短的同步.注意ManualResetEventSlim此類在等待時間較短的情況下采用的是旋轉的方式(在較短的等待中旋轉比等待句柄開銷要小的多),在等待時間較長的情況下采用的是等待句柄的方式.
2.8.8.4 AutoResetEvent和ManualResetEvent區別
AutoResetEvent和ManualResetEvent的區別在于當AutoResetEvent調用Set()之后會自動重置調用Reset()方法.而ManualResetEvent方法在調用了Set()之后需要手動調用Reset()方法否則只要在ManualResetEvent上調用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());
}
}
});
}
}
結果:
這個例子有點類似生產者和消費者的例子.
2.8.8.5 CountdownEvent
表示在計數變為零時會得到信號通知的同步基元.它在收到一定次數的信號之后,將會解除對其等待線程的鎖定. CountdownEvent 專門用于以下情況:您必須使用 ManualResetEvent 或 ManualResetEventSlim,并且必須在用信號通知事件之前手動遞減一個變量. 例如,在分叉/聯接方案中,您可以只創建一個信號計數為 5 的CountdownEvent,然后在線程池上啟動五個工作項,并且讓每個工作項在完成時調用 Signal. 每次調用 Signal 時,信號計數都會遞減 1. 在主線程上,對 Wait 的調用將會阻塞,直至信號計數為零.
例子:
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();
}
}
輸出結果:
注意此類的Wait(CancellationTokencancellationToken)方法的重載中是可以傳入一個取消等待的對象.
2.8.9 ReaderWriterLock/ReaderWriterLockSlim鎖
這兩個類都是用于定義一個寫和多個讀的加鎖方式.這兩個類都適用于對一個不經常發生變化的資源進行加鎖.在性能上使用這兩個類加鎖一個不經常變化的資源比直接使用monitor,mutex等要高出很多.ReaderWriterLockSlim在性能上比ReaderWriterLock要高,他簡化了遞歸,升級和降級的鎖定規則,同時可以避免潛在的死鎖.因此在實際的項目中建議使用ReaderWriterLockSlim類.下面就只介紹ReaderWriterLockSlim類.
ReaderWriterLockSlim類可以利用EnterReadLock, TryEnterReadLock, EnterWriteLock和TryEnterWriteLock獲取當前是讀寫的鎖.還可以使用 EnterUpgradeableReadLock或者TryEnterUpgradeableReadLock函數類進行先讀后寫的操作(MSDN中把這個叫做升級或者降級,這點比較復雜不做深入說明,喜歡的朋友可以自己去MSDN看).這個函數可以讓讀鎖升級,從而不需要釋放讀鎖.注意在調用了Entry…這類方法之后必須調用對應的exit的方法,例如調用了EnterReadLock之后需要調用ExitReadLock;調用了EnterWriteLock之后需要調用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})正在艸,讀就排隊等排隊等待艸的數量:{1}"
, Thread.CurrentThread.ManagedThreadId,lckSlim.WaitingWriteCount));
Console.WriteLine("等待讀的排隊數量:" + lckSlim.WaitingReadCount);
}
Console.WriteLine(string.Format("當前線程{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("當前線程{0}干完收工:",Thread.CurrentThread.ManagedThreadId));
lckSlim.ExitWriteLock();
}
}
}
輸出結果:
注意:此類中的從讀模式升級的時候是一個復雜的過程,我沒搞明白就不在多說.
2.8.10 Barrier 鎖
使多個任務能夠采用并行方式依據某種算法在多個階段中協同工作.這個是什么意思呢,就是說比如abcd四個任務分別有1,2,3這個三個階段來.每個任務必須等到其他任務完成當前階段的之后才能夠繼續下一階段的任務.
例子如下:
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);
}
}
輸出結果:
注意在調用Barrier類的時候必須指定參與線程的數量.
在使用Barrier類的時候有一個稍微不好弄的就是異常處理. 如果進入屏障后,工作的代碼出現了異常,這個異常會被放在BarrierPostPhaseException中,而且所有任務都能夠捕捉到這個異常.原始的異常可以通過NarrierPostPhaseException 對象的InnerException進行訪問.
2.9 線程總結
線程在實際的開發中使用相當的多.在這個過程中使用鎖的時候也非常的多.在選則使用線程的時候可以優先考慮使用Task提供功能來完成(微軟Framework自身的代碼中很多多線程的功能都是調用的Task來完成).其次是直接使用線程池.至于最簡單的Timer其實是最不可靠的(可能是我不太會使用).
線程同步稍微復雜一點.這個復雜的表現有幾個方面:1同步的時會影響性能.這點在加鎖的時候就需要考慮在不通的環境使用不通的鎖.在沒有必要加鎖的地方就不要使用鎖.2 同步中容易發生死鎖.這個問題一旦發現而且不容易被發現.有些時候代碼跑幾天都不出現問題.因此查找問題的難度增加.一旦出現這樣的問題請先梳理整個流程.檢查有沒有互相掙鎖的情況.第二可以寫日志來查找.在使用鎖的時候盡量的小粒度加鎖.
文章pdf下載地址:
http://download.csdn.net/detail/shibinysy/9712624
新聞熱點
疑難解答