上一篇主要介紹了進程和線程的一些基本知識,現在回歸正題,我們來學一下線程的使用,本篇主要是使用新建線程和線程池的方式。 線程 先來介紹簡單的線程使用:使用new方法來創建線程,至於撤銷線程,我們不必去管(我也不知道怎麼去管XD),因為CLR已經替我們去管理了。 創建 先來看一個簡單的使用線程的例子: ...
上一篇主要介紹了進程和線程的一些基本知識,現在回歸正題,我們來學一下線程的使用,本篇主要是使用新建線程和線程池的方式。
線程
先來介紹簡單的線程使用:使用new方法來創建線程,至於撤銷線程,我們不必去管(我也不知道怎麼去管XD),因為CLR已經替我們去管理了。
創建
先來看一個簡單的使用線程的例子:
static void Main(string[] args) { Thread t1 = new Thread(Menthod1); Thread t2 = new Thread(Menthod2); t1.Start(); t2.Start("線程2參數"); Console.WriteLine("主線程的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("--------------------"); Console.ReadLine(); } static void Menthod1() { Thread.Sleep(2000); Console.WriteLine("線程1的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("--------------------"); } static void Menthod2(object obj) { Thread.Sleep(1000); Console.WriteLine("線程2的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("obj:{0}", obj); Console.WriteLine("--------------------"); }
我們可以用過new的方式創建一個線程,然後使用Start()的方法來運行該線程,線程則會在其生命周期去執行Method1方法,執行方法肯定需要時間的,但是Method1的方法過於簡單,我們使用Thread.Sleep的方法來進行停頓,這個方法可以暫時將當前的線程睡眠一段時間(毫秒為單位),因為主線程只是創建並運行t1子線程,運行任務的不是主線程,所以主線程可以繼續往後執行程式。
我們還可以向線程執行的方法傳入一個參數,例如線程2,在t2執行Start方法時,傳入想要傳入的參數,然後就可以在運行的時候使用了;不過參數是有限制的,在子線程的方法只能接受object的類型的參數,則在使用的時候需要顯式轉換類型,還有就是只能接受一個參數,多個參數也不會支持。
線程與Lambda表達式
線程的new也支持Lambda表達式,若是執行方法比較簡單,或者在某些場景下,我們可以將線程執行的代碼使用Lambda內置到新建裡面:
static void Main(string[] args) { Thread t1 = new Thread(() => { Thread.Sleep(2000); Console.WriteLine("線程1的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("--------------------"); }); t1.Start(); Console.WriteLine("主線程的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("--------------------"); Console.ReadLine(); }
這裡這樣子寫還有一個好處,就是這裡可以直接使用主方法裡面的變數,當然,這也會產生線程安全的問題。
線程同步
一個進程中的多個線程都是可以訪問其進程的其他資源,多線程若不加以控制也是併發執行的,若在多線程的執行方法中包含操作全局變數、者靜態變數或是使用I/O設備的時候,很容易的就會產生線程安全的問題,從而導致不可預估的錯誤。這裡就需要進行線程同步了,下麵介紹一些線程同步的方式。
Join:
我們有時候開啟了n各子線程來進行輔助計算,但是又想主線程等待所有子線程計算完畢在接著執行,或者線程之間的關係更複雜,其中涉及了線程的阻塞與激活,那麼就可以使用Join()的方法來阻塞主線程,實現一種最簡單的線程同步:
static void Main(string[] args) { Thread t1 = new Thread(Menthod1); Thread t2 = new Thread(Menthod2); t1.Start(); t1.Join(); t2.Start("線程2參數"); t2.Join(); Console.WriteLine("主線程的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("--------------------"); Console.ReadLine(); } static void Menthod1() { Thread.Sleep(2000); Console.WriteLine("線程1的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("--------------------"); } static void Menthod2(object obj) { Thread.Sleep(4000); Console.WriteLine("線程2的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("obj:{0}", obj); Console.WriteLine("--------------------"); }
上面的調用阻塞的過程:先是t1開始,阻塞2秒,再接著t2執行,阻塞4秒,共計阻塞6秒,貌似沒有發揮出來多線程的優勢,但是也有可能在t2運行之前必須運行完t1,所以,Join()的調用需要視情況而定,Join()就是阻塞當前線程到當前位置,直到阻塞線程結束後,當前線程繼續運行。
同步事件:
除了Join()來實現線程間的阻塞與激活,還有同步事件來進行處理;同步事件有兩種:AutoResetEvent和 ManualResetEvent。它們之間唯一不同的地方就是在激活線程之後,狀態是否自動由終止變為非終止。AutoResetEvent自動變為非終止,就是說一個AutoResetEvent只能激活一個線程。而ManualResetEvent要等到它的Reset方法被調用,狀態才變為非終止,在這之前,ManualResetEvent可以激活任意多個線程:先來看ManualResetEvent的使用:
static ManualResetEvent muilReset = new ManualResetEvent(false); static void Main(string[] args) { Thread t1 = new Thread(Menthod1); t1.Start(); Thread t2 = new Thread(Menthod2); t2.Start("params"); Thread t3 = new Thread(Menthod3); t3.Start(); muilReset.WaitOne(); Console.WriteLine("主線程的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("--------------------"); Console.ReadLine(); } static void Menthod1() { muilReset.WaitOne(); Console.WriteLine("線程1的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("--------------------"); } static void Menthod2(object obj) { muilReset.WaitOne(); Console.WriteLine("線程2的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("obj:{0}", obj); Console.WriteLine("--------------------"); } static void Menthod3() { Thread.Sleep(3000); Console.WriteLine("線程3的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("激活線程..."); Console.WriteLine("--------------------"); muilReset.Set(); }
上面例子我們將主線程、線程1、線程2阻塞,使用線程3在3秒鐘之後激活全部線程,顯示成功
線程3的ID:12 激活線程... -------------------- 線程2的ID:11 obj:params -------------------- 主線程的ID:9 -------------------- 線程1的ID:10 --------------------
若是使用AutoResetEvent則只能激活主線程
線程3的ID:12 激活線程... -------------------- 主線程的ID:9 --------------------
註:ManualResetEvent會給所有引用的線程都發送一個信號(多個線程可以共用一個ManualResetEvent,當ManualResetEvent調用Set()時,所有線程將被喚醒),而AutoResetEvent只會隨機給其中一個發送信號(只能喚醒一個)。
這裡的線程同步還可以使用委托與事件(推薦使用事件)來實現線程間的簡單通訊,比如在某一線程執行到某一結點後,通過事件向另一個或者多個線程發送更多的信息。
Monitor:
上述的例子是各個子線程之間沒有使用公共資源(公共變數、I/O設備等),它們只存在執行順序上的先後;我們來找一個使用公共變數的例子試一試:
static List<int> ids = new List<int>(); static void Main(string[] args) { Console.WriteLine("主線程的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("--------------------"); for (int i = 0; i < 100; i++) { Thread t = new Thread(Menthod); t.Start(); } Console.ReadLine(); } static void Menthod() { ids.Add(Thread.CurrentThread.ManagedThreadId); Console.WriteLine(ids.Count); Console.WriteLine(ids[0]); Console.WriteLine("--------------------"); ids.Clear(); }
這裡新建100個子線程,然後使用一個靜態公共變數List輸出線程的Id,以上述方法運行時,有時會報出錯誤:索引超出範圍。必須為非負值並小於集合大小!說明當前子線程輸出Id時,該集合被Clear掉了,這就是一個很簡單的線程安全問題,所以需要使用Monitor來進行鎖住代碼塊,MSDN推薦定義一個私有的初始化不會再變的object變數作為一個排他鎖,因為排他鎖變了就沒意義了,下麵代碼就可以變為:
static readonly object locker = new object(); static List<int> ids = new List<int>(); static void Main(string[] args) { Console.WriteLine("主線程的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("--------------------"); for (int i = 0; i < 100; i++) { Thread t = new Thread(Menthod); t.Start(); } Console.ReadLine(); } static void Menthod() { Monitor.Enter(locker); ids.Add(Thread.CurrentThread.ManagedThreadId); Console.WriteLine(ids.Count); Console.WriteLine(ids[0]); Console.WriteLine("--------------------"); ids.Clear(); Monitor.Exit(locker); }
當有一個線程進入鎖住的代碼塊是,是在外面加鎖,這樣剩下的線程只能等待當前線程執行完畢後釋放鎖,這樣的話就保證了List變數在取值時不會被其他線程清除掉;儘管List是一個線程安全類,就是多線程操作該類時只有一個線程操作的類,但是這裡仍然避免不了線程安全的問題,因為仍然控制不了操作的順序,在清除後讀取肯定會報錯。
lock:
調用Monitor執行只能有一個線程運行的代碼塊時,仍有可能會拋出異常,但是有時候又不能終止進程,使用try{}catch{}包起來是個解決方式,那乾脆再封裝一次Monitor的方法,於是lock便出現了,則上述的例子可以改寫為:
static void Menthod() { lock (locker) { ids.Add(Thread.CurrentThread.ManagedThreadId); Console.WriteLine(ids.Count); Console.WriteLine(ids[0]); Console.WriteLine("--------------------"); ids.Clear(); } }
等價於:
static void Menthod() { try { Monitor.Enter(locker); ids.Add(Thread.CurrentThread.ManagedThreadId); Console.WriteLine(ids.Count); Console.WriteLine(ids[0]); Console.WriteLine("--------------------"); ids.Clear(); } catch (Exception ex) { } finally { Monitor.Exit(locker); } }
當線程進入lock代碼塊時,將會調用Monitor.Enter()方法,退出代碼塊會調用Monitor.Exit()方法。另外,Monitor還提供了三個靜態方法Monitor.Pulse(),Monitor.PulseAll()和Monitor.Wait() ,用來實現一種喚醒機制的同步。關於這三個方法的用法,可以參考MSDN,我這裡也在學習中,就先不講述了。雖說lock沒有Monitor功能強大,但是使用確實方便,這裡取捨就看實際需求了。
補充
線程同步的方式還有很多,比如Mutex。還有很多的方法,以後用到的時候在研究吧。
Mutex:Mutex不具備Wait,Pulse,PulseAll的功能,因此,我們不能使用Mutex實現類似的喚醒的功能;不過Mutex有一個比較大的特點,Mutex是跨進程的,因此我們可以在同一臺機器甚至遠程的機器上的多個進程上使用同一個互斥體。
線程池
目的
上一篇內容提到,線程是由線程ID、程式計數器、寄存器集合和堆棧組成,是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程自己不擁有系統資源,只擁有一些在運行中必不可少的資源;這就意味著線程在進行創建與撤銷的時候,都需要分配與清空一些資源,總歸需要付出一定量的時空消耗;在一些大量使用線程(CPU密集、I/O密集)的進程裡面,使用傳統的new方法會頻繁的創建、撤銷線程,雖說線程的管理是由CLR來進行的,但是總歸是影響性能,為了減少創建與撤銷的時空消耗,便引入了線程池的概念:將線程實體池化,就是事先創建一定量的線程實體,然後放到一個容器中,做統一管理,沒有任務時,線程處於空閑狀態(差不多就是就緒狀態),來任務後選擇一個空閑線程來執行,執行完畢後自動關閉線程(沒有被撤銷,只是置為空閑狀態)。
CLR線程池
CLR線程池是.NET框架中很重要的一部分,不光能被開發人員使用,自身的很多功能也是由線程池實現;我們在將任務委托給線程池的時候,是將該任務放到線程池的任務隊列上,若線程池記憶體在空閑線程,則會將該任務委托給該線程,等待調度到CPU執行,若是沒有空閑的線程且線程池所管理的線程數量還沒有達到上限的時候,線程池便會創建新的Thread實體,否則,該任務會在隊列中等待。
數量上限:在CLR 2.0 SP1之前的版本中,線程池中 預設最大的線程數量 = 處理器數 * 25, CLR 2.0 SP1之後就變成了 預設最大線程數量 = 處理器數 * 250,線程上限可以改變,通過使用ThreadPool.GetMax+Threads和ThreadPool.SetMaxThreads方法,可以獲取和設置線程池的最大線程數。
使用
線程池的使用更簡單一些:
static void Main(string[] args) { ThreadPool.QueueUserWorkItem(new WaitCallback(Menthod1)); ThreadPool.QueueUserWorkItem(new WaitCallback(Menthod2), "object"); Console.WriteLine("主線程的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("--------------------"); Console.ReadLine(); } static void Menthod1(object obj) { Thread.Sleep(2000); Console.WriteLine("線程1的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("--------------------"); } static void Menthod2(object obj) { Thread.Sleep(4000); Console.WriteLine("線程2的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("obj:{0}", obj); Console.WriteLine("--------------------"); }
這裡QueueUserWorkItem方法需要傳入一個QueueUserWorkItem委托(帶object類型的參數,無返回值),所以我們需要線程執行的任務需要帶一個object的參數,並且QueueUserWorkItem方法加入時存在一個重載,可以在這裡傳入一個參數。
當然這裡也可以使用Lambda表達式:
ThreadPool.QueueUserWorkItem(new WaitCallback((object obj)=>{ Thread.Sleep(2000); Console.WriteLine("線程3的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("obj:{0}", obj); Console.WriteLine("--------------------"); }), "lambda");
線程同步
使用線程池併發執行任務同樣會遇到線程安全的問題,一樣需要進行同步,在涉及線程使用公共資源,Monitor、lock等方法與上述線程使用一樣,同樣能達到理想的效果,就不重覆介紹了;但是對於控制執行順序上,這個沒有使用new線程來的自由。
同步事件:
線上程池中,沒有Join方法,若想控制線程的執行順序,我推薦使用主線程等待線程池任務執行完畢,阻塞主線程的方式,這裡可以使用WaitHandle:
static void Main(string[] args) { List<WaitHandle> handles = new List<WaitHandle>(); AutoResetEvent autoReset1 = new AutoResetEvent(false); ThreadPool.QueueUserWorkItem(new WaitCallback(Menthod1), autoReset1); handles.Add(autoReset1); AutoResetEvent autoReset2 = new AutoResetEvent(false); ThreadPool.QueueUserWorkItem(new WaitCallback(Menthod2), autoReset2); handles.Add(autoReset2); WaitHandle.WaitAll(handles.ToArray()); Console.WriteLine("主線程的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("--------------------"); Console.ReadLine(); } static void Menthod1(object obj) { Thread.Sleep(2000); Console.WriteLine("線程1的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("--------------------"); AutoResetEvent handle = (AutoResetEvent)obj; handle.Set(); } static void Menthod2(object obj) { Thread.Sleep(4000); Console.WriteLine("線程2的ID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("--------------------"); AutoResetEvent handle = (AutoResetEvent)obj; handle.Set(); }
在這裡,給線程池每個相關的線程都創建一個AutoResetEvent,在執行完畢之後分別把屬於自己的AutoResetEvent變為非終止,WaitHandle使用WaitAll方法阻塞主線程、等待所有的AutoResetEvent事件變為true,另外WaitHandle還有一個WaitAny方法阻塞,不過是只要其中一個線程結束,就會繼續運行,不再阻塞。
註:
1、WaitHandle同樣可以用於new創建線程的同步事件;
2、WaitHandle等待方法(WaitAll、WaitAny)的數組長度的數目必須少於或等於 64 個,為瞭解決此限制,有網友封裝了一個類,比較好用:
public class MutipleThreadResetEvent : IDisposable { private readonly ManualResetEvent done; private readonly int total; private long current; /// <summary> /// 構造函數 /// </summary> /// <param name="total">需要等待執行的線程總數</param> public MutipleThreadResetEvent(int total) { this.total = total; current = total; done = new ManualResetEvent(false); } /// <summary> /// 喚醒一個等待的線程 /// </summary> public void SetOne() { // Interlocked 原子操作類 ,此處將計數器減1 if (Interlocked.Decrement(ref current) == 0) { //當所以等待線程執行完畢時,喚醒等待的線程 done.Set(); } } /// <summary> /// 等待所有線程執行完畢 /// </summary> public void WaitAll() { done.WaitOne(); } /// <summary> /// 釋放對象占用的空間 /// </summary> public void Dispose() { ((IDisposable)done).Dispose(); } }MutipleThreadResetEvent
補充
1、線程有前臺線程和後臺線程之分,使用new創建的線程預設為前臺線程(可以使用IsBackground屬性來進行更改),線程池裡面都是後臺線程
前臺線程:前臺線程是不會被立即關閉的,它的關閉只會發生在自己執行完成時,不受外在因素的影響。假如應用程式退出,造成它的前臺線程終止,此時CLR仍然保持活動並運行,使應用程式能繼續運行,當它的的前臺線程都終止後,整個進程才會被銷毀。
後臺線程:後臺線程是可以隨時被CLR關閉而不引發異常的,也就是說當後臺線程被關閉時,資源的回收是立即的,不等待的,也不考慮後臺線程是否執行完成,就算是正在執行中也立即被終止。
2、線程被系統調度到CPU執行時存在優先順序:這裡的優先順序不是優先執行,而是被調度到CPU執行的概率高;使用new創建線程與線程池的優先順序預設都是Normal,不過前者可以通過Priority屬性來設置優先順序。優先順序有5個級別:Highest、AboveNormal、Normal、BelowNormal和Lowest。
3、線程存在Suspend與Resume這兩個過時的方法,但不是代表不能使用,只是微軟不推薦你用,MSDN給出的原因是:請不要使用 Suspend 和 Resume 方法來同步線程活動。 沒有辦法知道當你暫停執行線程什麼代碼。 如果在安全許可權評估期間持有鎖,您掛起線程中的其他線程 AppDomain 可能被阻止。 如果執行類構造函數時,您掛起線程中的其他線程 AppDomain 中嘗試使用類被阻止。 可以很容易發生死鎖。你可以無視這個警告繼續使用這兩個方法進行線程同步,若覺得不怎麼靠譜,那麼可以線上程代碼加入判斷來保證執行正確性,或者使用控制同步事件(AutoResetEvent等)來實現線程同步。
4、線程池的線程很珍貴,因為數量是有限的,所以不適合執行長時間的作業任務,適合執行短期並且頻繁的作業任務,若想執行長時間的作業任務,建議使用new創建新線程的方式。畢竟線程池設計的初衷就是為瞭解決頻繁創建與撤銷線程而造成的資源浪費。