先表明,向作者致敬http://www.cnblogs.com/leslies2/archive/2012/02/07/2310495.html 風(fēng)塵浪子 前半部分是復(fù)制風(fēng)塵浪子的,從 三 開始,互聯(lián)網(wǎng)收集整理. 感謝互聯(lián)網(wǎng),感謝open source. 重要是,大家能夠領(lǐng)悟,掌握和運(yùn)用多線程的知識.
1. 1 進(jìn)程、應(yīng)用程序域與線程的關(guān)系 進(jìn)程(PRocess)是Windows系統(tǒng)中的一個(gè)基本概念,它包含著一個(gè)運(yùn)行程序所需要的資源。進(jìn)程之間是相對獨(dú)立的,一個(gè)進(jìn)程無法訪問另一個(gè) 進(jìn)程的數(shù)據(jù)(除非利用分布式計(jì)算方式),一個(gè)進(jìn)程運(yùn)行的失敗也不會影響其他進(jìn)程的運(yùn)行,Windows系統(tǒng)就是利用進(jìn)程把工作劃分為多個(gè)獨(dú)立的區(qū)域的。進(jìn) 程可以理解為一個(gè)程序的基本邊界。 應(yīng)用程序域(AppDomain)是一個(gè)程序運(yùn)行的邏輯區(qū)域,它可以視為一個(gè)輕量級的進(jìn)程,.NET的程序集正是在應(yīng)用程序域中運(yùn)行的,一個(gè)進(jìn)程可 以包含有多個(gè)應(yīng)用程序域,一個(gè)應(yīng)用程序域也可以包含多個(gè)程序集。在一個(gè)應(yīng)用程序域中包含了一個(gè)或多個(gè)上下文context,使用上下文CLR就能夠把某些 特殊對象的狀態(tài)放置在不同容器當(dāng)中。 線程(Thread)是進(jìn)程中的基本執(zhí)行單元,在進(jìn)程入口執(zhí)行的第一個(gè)線程被視為這個(gè)進(jìn)程的主線程。在.NET應(yīng)用程序中,都是以Main()方法 作為入口的,當(dāng)調(diào)用此方法時(shí)系統(tǒng)就會自動創(chuàng)建一個(gè)主線程。線程主要是由CPU寄存器、調(diào)用棧和線程本地存儲器(Thread Local Storage,TLS)組成的。CPU寄存器主要記錄當(dāng)前所執(zhí)行線程的狀態(tài),調(diào)用棧主要用于維護(hù)線程所調(diào)用到的內(nèi)存與數(shù)據(jù),TLS主要用于存放線程的狀 態(tài)信息。 進(jìn)程、應(yīng)用程序域、線程的關(guān)系如下圖,一個(gè)進(jìn)程內(nèi)可以包括多個(gè)應(yīng)用程序域,也有包括多個(gè)線程,線程也可以穿梭于多個(gè)應(yīng)用程序域當(dāng)中。但在同一個(gè)時(shí)刻,線程只會處于一個(gè)應(yīng)用程序域內(nèi)。
在單CPU系統(tǒng)的一個(gè)單位時(shí)間(time slice)內(nèi),CPU只能運(yùn)行單個(gè)線程,運(yùn)行順序取決于線程的優(yōu)先級別。如果在單位時(shí)間內(nèi)線程未能完成執(zhí)行,系統(tǒng)就會把線程的狀態(tài)信息保存到線程的本地 存儲器(TLS) 中,以便下次執(zhí)行時(shí)恢復(fù)執(zhí)行。而多線程只是系統(tǒng)帶來的一個(gè)假像,它在多個(gè)單位時(shí)間內(nèi)進(jìn)行多個(gè)線程的切換。因?yàn)榍袚Q頻密而且單位時(shí)間非常短暫,所以多線程可 被視作同時(shí)運(yùn)行。 適當(dāng)使用多線程能提高系統(tǒng)的性能,比如:在系統(tǒng)請求大容量的數(shù)據(jù)時(shí)使用多線程,把數(shù)據(jù)輸出工作交給異步線程,使主線程保持其穩(wěn)定性去處理其他問題。但需要注意一點(diǎn),因?yàn)镃PU需要花費(fèi)不少的時(shí)間在線程的切換上,所以過多地使用多線程反而會導(dǎo)致性能的下降。
2.1 System.Threading.Thread類 System.Threading.Thread是用于控制線程的基礎(chǔ)類,通過Thread可以控制當(dāng)前應(yīng)用程序域中線程的創(chuàng)建、掛起、停止、銷毀。 它包括以下常用公共屬性:
屬性名稱 | 說明 |
---|---|
CurrentContext | 獲取線程正在其中執(zhí)行的當(dāng)前上下文。 |
CurrentThread | 獲取當(dāng)前正在運(yùn)行的線程。 |
ExecutionContext | 獲取一個(gè) ExecutionContext 對象,該對象包含有關(guān)當(dāng)前線程的各種上下文的信息。 |
IsAlive | 獲取一個(gè)值,該值指示當(dāng)前線程的執(zhí)行狀態(tài)。 |
IsBackground | 獲取或設(shè)置一個(gè)值,該值指示某個(gè)線程是否為后臺線程。 |
IsThreadPoolThread | 獲取一個(gè)值,該值指示線程是否屬于托管線程池。 |
ManagedThreadId | 獲取當(dāng)前托管線程的唯一標(biāo)識符。 |
Name | 獲取或設(shè)置線程的名稱。 |
Priority | 獲取或設(shè)置一個(gè)值,該值指示線程的調(diào)度優(yōu)先級。 |
ThreadState | 獲取一個(gè)值,該值包含當(dāng)前線程的狀態(tài)。 |
2.1.1 線程的標(biāo)識符 ManagedThreadId是確認(rèn)線程的唯一標(biāo)識符,程序在大部分情況下都是通過Thread.ManagedThreadId來辨別線程的。 而Name是一個(gè)可變值,在默認(rèn)時(shí)候,Name為一個(gè)空值 Null,開發(fā)人員可以通過程序設(shè)置線程的名稱,但這只是一個(gè)輔助功能。
2.1.2 線程的優(yōu)先級別 .NET為線程設(shè)置了Priority屬性來定義線程執(zhí)行的優(yōu)先級別,里面包含5個(gè)選項(xiàng),其中Normal是默認(rèn)值。除非系統(tǒng)有特殊要求,否則不應(yīng)該隨便設(shè)置線程的優(yōu)先級別。
成員名稱 | 說明 |
---|---|
Lowest | 可以將 Thread 安排在具有任何其他優(yōu)先級的線程之后。 |
BelowNormal | 可以將 Thread 安排在具有 Normal 優(yōu)先級的線程之后,在具有 Lowest 優(yōu)先級的線程之前。 |
Normal | 默認(rèn)選擇。可以將 Thread 安排在具有 AboveNormal 優(yōu)先級的線程之后,在具有 BelowNormal 優(yōu)先級的線程之前。 |
AboveNormal | 可以將 Thread 安排在具有 Highest 優(yōu)先級的線程之后,在具有 Normal 優(yōu)先級的線程之前。 |
Highest | 可以將 Thread 安排在具有任何其他優(yōu)先級的線程之前。 |
2.1.3 線程的狀態(tài) 通過ThreadState可以檢測線程是處于Unstarted、Sleeping、Running 等等狀態(tài),它比 IsAlive 屬性能提供更多的特定信息。 前面說過,一個(gè)應(yīng)用程序域中可能包括多個(gè)上下文,而通過CurrentContext可以獲取線程當(dāng)前的上下文。 CurrentThread是最常用的一個(gè)屬性,它是用于獲取當(dāng)前運(yùn)行的線程。
2.1.4 System.Threading.Thread的方法 Thread 中包括了多個(gè)方法來控制線程的創(chuàng)建、掛起、停止、銷毀,以后來的例子中會經(jīng)常使用。
方法名稱 | 說明 |
---|---|
Abort() | 終止本線程。 |
GetDomain() | 返回當(dāng)前線程正在其中運(yùn)行的當(dāng)前域。 |
GetDomainId() | 返回當(dāng)前線程正在其中運(yùn)行的當(dāng)前域Id。 |
Interrupt() | 中斷處于 WaitSleepJoin 線程狀態(tài)的線程。 |
Join() | 已重載。 阻塞調(diào)用線程,直到某個(gè)線程終止時(shí)為止。 |
Resume() | 繼續(xù)運(yùn)行已掛起的線程。 |
Start() | 執(zhí)行本線程。 |
Suspend() | 掛起當(dāng)前線程,如果當(dāng)前線程已屬于掛起狀態(tài)則此不起作用 |
Sleep() | 把正在運(yùn)行的線程掛起一段時(shí)間。 |
2.1.5 開發(fā)實(shí)例 以下這個(gè)例子,就是通過Thread顯示當(dāng)前線程信息
static void Main(string[] args) { Thread thread = Thread.CurrentThread; thread.Name = "Main Thread"; string threadMessage = string.Format("Thread ID:{0}/n Current AppDomainId:{1}/n " + "Current ContextId:{2}/n Thread Name:{3}/n " + "Thread State:{4}/n Thread Priority:{5}/n", thread.ManagedThreadId, Thread.GetDomainID(), Thread.CurrentContext.ContextID, thread.Name, thread.ThreadState, thread.Priority); Console.WriteLine(threadMessage); Console.ReadKey(); }3.1使用ThreadStart
static void Main(string[] args) { //Thread th = new Thread(Sleep); Thread th = new Thread(new ThreadStart(Sleep)); th.Start(); for (int i = 0; i < 10; i++) { Console.WriteLine("這里是主線程在工作" + i); } } //模擬執(zhí)行長時(shí)間的任務(wù) static void Sleep() { ThreadMessage("Sleep"); Console.WriteLine("我還沒有執(zhí)行完呢,請耐心等待...."); Thread.Sleep(3000); } static void ThreadMessage(string data) { string message = string.Format("ThreadName is {0} ThreadId is:{1}", data, Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); } 運(yùn)行后你會發(fā)現(xiàn),Sleep()方法 還沒有執(zhí)行完(睡眠3分鐘模擬長時(shí)間任務(wù)),主線程已經(jīng)執(zhí)行完成. 這就是多線程的好處. 3.2 ParameterizedThreadStart 帶參數(shù)的
運(yùn)行結(jié)果和上面那差不多一樣.
3.3 匿名函數(shù)(委托)在多線程的運(yùn)用 想必大家發(fā)現(xiàn)了這2個(gè)例子中都有一句注釋的Thread th = new Thread(Sleep),這是為什么呢? 你可以把注釋打開,下面的th注釋掉,運(yùn)行下,你會發(fā)現(xiàn),兩次都一樣,這又是為什么呢?因?yàn)樵趯?shí)例化th的時(shí)候,有4個(gè)重載方法,其中2個(gè)就是ThreadStart和ParameterizedThreadStart 我們都知道ThreadStart和ParameterizedThreadStart和2個(gè)委托,而委托最大的作用就是傳遞一個(gè)方法, 在C#2.0就引入了匿名方法(3.0以及更高,lambda表達(dá)式取代了匿名方法)我們來看看匿名方法在這里能給我們帶來點(diǎn)什么驚喜(方便)
上面的幾種方式都可以用. 我為什么要用個(gè)ShowMessage(string msg) 方法來測試呢 ? 細(xì)心你會發(fā)現(xiàn)ParameterizedThreadStart委托他定義的參數(shù)為object,用匿名函數(shù)就可以解決這問題.如果你對委托,匿名函數(shù)不太熟悉的話,你就要補(bǔ)習(xí)一下關(guān)于委托的知識了.
3.4 前臺線程and后臺線程 注意以上兩個(gè)例子都沒有使用Console.ReadKey(),但系統(tǒng)依然會等待異步線程完成后才會結(jié)束。這是因?yàn)槭褂肨hread.Start()啟動的線程默認(rèn)為前臺線程,而系統(tǒng)必須等待所有前臺線程運(yùn)行結(jié)束后,應(yīng)用程序域才會自動卸載。 在第二節(jié)曾經(jīng)介紹過線程Thread有一個(gè)屬性IsBackground,通過把此屬性設(shè)置為true,就可以把線程設(shè)置為后臺線程!這時(shí)應(yīng)用程序域?qū)⒃谥骶€程完成時(shí)就被卸載(主線程關(guān)閉),而不會等待異步線程的運(yùn)行。
3.5 線程的一些方法 Thread.Sleep()大家都很熟悉了,休眠多長時(shí)間,里面是毫秒.1秒=1000毫秒. Join() 表面意思是把線程加入,也就是這線程完事后主線程才被卸載. 你可以把子線程設(shè)置成后臺線程,然后調(diào)用這個(gè)方法,就不會發(fā)現(xiàn)’一閃而過’的現(xiàn)象了. Thread.Suspend()與 Thread.Resume()是在Framework1.0 就已經(jīng)存在的老方法了,它們分別可以掛起、恢復(fù)線程。但在Framework2.0中就已經(jīng)明確排斥這兩個(gè)方法。這是因?yàn)橐坏┠硞€(gè)線程占用了已有的資源,再使用Suspend()使線程長期處于掛起狀態(tài),當(dāng)在其他線程調(diào)用這些資源的時(shí)候就會引起死鎖!所以在沒有必要的情況下應(yīng)該避免使用這兩個(gè)方法。 若想終止正在運(yùn)行的線程,可以使用Abort()方法。在使用Abort()的時(shí)候,將引發(fā)一個(gè)特殊異常 ThreadAbortException 。 若想在線程終止前恢復(fù)線程的執(zhí)行,可以在捕獲異常后 ,在catch(ThreadAbortException ex){…} 中調(diào)用Thread.ResetAbort()取消終止。 下面的例子是 終止線程和取消終止的例子(拷貝風(fēng)塵浪子的)
class Program { static void Main(string[] args) { Console.WriteLine("Main threadId is:" + Thread.CurrentThread.ManagedThreadId); Thread thread = new Thread(new ThreadStart(AsyncThread)); thread.Start(); Console.ReadKey(); } //以異步方式調(diào)用 static void AsyncThread() { try { string message = string.Format("/nAsync threadId is:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); for (int n = 0; n < 10; n++) { //當(dāng)n等于4時(shí),終止線程 if (n >= 4) { Thread.CurrentThread.Abort(n); } Thread.Sleep(300); Console.WriteLine("The number is:" + n.ToString()); } } catch (ThreadAbortException ex) { //輸出終止線程時(shí)n的值 if (ex.ExceptionState != null) Console.WriteLine(string.Format("Thread abort when the number is: {0}!", ex.ExceptionState.ToString())); //取消終止,繼續(xù)執(zhí)行線程 Thread.ResetAbort(); Console.WriteLine("Thread ResetAbort!"); } //線程結(jié)束 Console.WriteLine("Thread Close!"); } }到此,我們學(xué)會了運(yùn)用多線程,可不能沾沾自喜,這可只是剛剛開始. 前面說了通過ThreadStart創(chuàng)建的線程比較難管理,創(chuàng)建過多性能也會下降.主要是因?yàn)門hreadStart創(chuàng)建的線程不能循環(huán)利用,比如我們For循環(huán)個(gè)list,每一個(gè)model都開啟個(gè)線程去執(zhí)行任務(wù),當(dāng)前面的線程執(zhí)行完了,也就銷毀了,后面的還是要重新創(chuàng)建,不停的創(chuàng)建線程是很耗時(shí)的.由此可見 .NET為線程管理專門設(shè)置了一個(gè)CLR線程池.
4.1 關(guān)于CLR線程池 使用ThreadStart與ParameterizedThreadStart建立新線程非常簡單,但通過此方法建立的線程難于管理,若建立過多的線程反而會影響系統(tǒng)的性能。 有 見及此,.NET引入CLR線程池這個(gè)概念。CLR線程池并不會在CLR初始化的時(shí)候立刻建立線程,而是在應(yīng)用程序要創(chuàng)建線程來執(zhí)行任務(wù)時(shí),線程池才初始 化一個(gè)線程。線程的初始化與其他的線程一樣。在完成任務(wù)以后,該線程不會自行銷毀,而是以掛起的狀態(tài)返回到線程池。直到應(yīng)用程序再次向線程池發(fā)出請求時(shí), 線程池里掛起的線程就會再度激活執(zhí)行任務(wù)。這樣既節(jié)省了建立線程所造成的性能損耗,也可以讓多個(gè)任務(wù)反復(fù)重用同一線程,從而在應(yīng)用程序生存期內(nèi)節(jié)約大量開銷. 4.2 工作者線程與I/O線程
CLR線程池分為工作者線程(workerThreads)與I/O線程 (completionPortThreads) 兩種,工作者線程是主要用作管理CLR內(nèi)部對象的運(yùn)作,I/O(Input/Output) 線程顧名思義是用于與外部系統(tǒng)交換信息,IO線程的細(xì)節(jié)將在下一節(jié)詳細(xì)說明。
通過ThreadPool.GetMax(out int workerThreads,out int completionPortThreads )和 ThreadPool.SetMax( int workerThreads, int completionPortThreads)兩個(gè)方法可以分別讀取和設(shè)置CLR線程池中工作者線程與I/O線程的最大線程數(shù)。在 Framework2.0中最大線程默認(rèn)為25*CPU數(shù),在Framewok3.0、4.0中最大線程數(shù)默認(rèn)為250*CPU數(shù),在近年 I3,I5,I7 CPU出現(xiàn)后,線程池的最大值一般默認(rèn)為1000、2000。 若想測試線程池中有多少的線程正在投入使用,可以通過ThreadPool.GetAvailableThreads( out int workerThreads,out int completionPortThreads ) 方法。
使用CLR線程池的工作者線程一般有兩種方式,一是直接通過 ThreadPool.QueueUserWorkItem() 方法,二是通過委托(異步操作是加入在線程池中的),下面將逐一細(xì)說。 4.3 通過QueueUserWorkItem啟動工作者線程 ThreadPool線程池中包含有兩個(gè)靜態(tài)方法可以直接啟動工作者線程: 一為 ThreadPool.QueueUserWorkItem(WaitCallback) 二為 ThreadPool.QueueUserWorkItem(WaitCallback,Object) 先把WaitCallback委托指向一個(gè)帶有Object參數(shù)的無返回值方法,再使用 ThreadPool.QueueUserWorkItem(WaitCallback) 就可以異步啟動此方法,此時(shí)異步方法的參數(shù)被視為null 。
class Program { static void Main(string[] args) { ThreadPool.QueueUserWorkItem(new WaitCallback(Sleep)); for (int i = 0; i < 10; i++) { Console.WriteLine("這里是主線程在工作" + i); } Console.ReadKey(); } static void Sleep(object state) { ThreadMessage("sleep"); Console.WriteLine("我還沒有執(zhí)行完呢,請耐心等待...."); Thread.Sleep(3000); } static void ThreadMessage(string data) { string message = string.Format("ThreadName is {0} ThreadId is:{1}", data, Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); } } 這是個(gè)不帶參數(shù)的, ThreadPool.QueueUserWorkItem()有2個(gè)參數(shù),第二個(gè)是個(gè)object類型
我們創(chuàng)建了一個(gè)Person類,在加入線程池的時(shí)候,我們傳了一個(gè)person , 在ShowMessage()方法中,得到傳過來的person,并顯示人的信息. 那在這里,我們可以效仿3.3匿名函數(shù)在線程池中的應(yīng)用呢? 答案是可以的. 4.4 線程池中的異常處理 多線程執(zhí)行任務(wù)的時(shí)候,有時(shí)候總是要出錯嘛, 重要的是,我們能catch的住異常.
這樣? No~ 結(jié)果會差強(qiáng)人意的,我們并沒有catch住他. 那該在哪try呢. 對, 在委托調(diào)用那方法中. 我們這樣干,
這樣,程序就不會報(bào)錯了. 如果有好幾個(gè)任務(wù)方法, 那我們要寫好幾個(gè)try{}catch{}這樣肯定不是我們想要的,看看下面的封裝
其中Execute()方法就是把ThreadPool.QueueUserWorkItem用匿名函數(shù)的方法進(jìn)行了封裝,catch住了異常.
我們在前面說過了,線程池中的線程,默認(rèn)是后臺線程,前面幾個(gè)例子,最后都有句Console.ReadKey(),就是如果不手動關(guān)閉主程序,主程序是不會自動關(guān)的. 這肯定不是我們想要的,那我們怎么判斷所有子線程運(yùn)行完畢后,關(guān)閉主線程呢 4.5 WaitHandle.WaitAll() 等待所有子線程完成后,關(guān)閉主線程 多個(gè)線程之間的協(xié)調(diào)工作
class Program { static void Main(string[] args) { WaitHandle[] waits = new WaitHandle[2] { new AutoResetEvent(false), new AutoResetEvent(false) }; Person person = new Person() { Name = "Somnus", Age = 10, Wait=waits[0] }; Person person2 = new Person() { Name = "cnblogs", Age = 20, Wait = waits[1] }; ThreadExecutor.Execute(new WaitCallback(ShowMessage),person); ThreadExecutor.Execute(new WaitCallback(ShowMessage), person2); for (int i = 0; i < 10; i++) { Console.WriteLine("這里是主線程在工作" + i); } WaitHandle.WaitAll(waits); //WaitHandle.WaitAny(waits); } static void ShowMessage(object state) { Person person = (Person)state; AutoResetEvent are = (AutoResetEvent)person.Wait; Console.WriteLine("學(xué)生姓名是{0},年齡為{1}", person.Name, person.Age); Thread.Sleep(3000); are.Set(); } } class Person { public string Name { get; set; } public int Age { get; set; } public WaitHandle Wait { get; set; } } public class ThreadExecutor { public static bool Execute(System.Threading.WaitCallback callback, object state) { try { return System.Threading.ThreadPool.QueueUserWorkItem((data) => { try { callback(data); } catch (Exception ex) { //寫日志 } }, state); } catch (Exception e) { //寫日志 } return false; } public static bool Execute(System.Threading.WaitCallback callback) { try { return System.Threading.ThreadPool.QueueUserWorkItem((data) => { try { callback(data); } catch (Exception ex) { //寫日志 } }); } catch (Exception e) { //寫日志 } return false; } } WaitHandle.WaitAll(),最大可監(jiān)測64個(gè)WaitHandler ,如果你需要的多線程比較多,你可以分批,中間Sleep()一段時(shí)間,就可以了. WaitHandle.WaitAny(),其中某一個(gè)線程完成后,就退出主線程.
4.6 委托類 使用CLR線程池中的工作者線程,最靈活最常用的方式就是使用委托的異步方法.委托包括下面3個(gè)重要方法:Invoke(),BeginInvoke(),EndInvoke() 當(dāng)調(diào)用Invoke()方法時(shí),對應(yīng)此委托的所有方法都會被執(zhí)行。而BeginInvoke與EndInvoke則支持委托方法的異步調(diào)用,由BeginInvoke啟動的線程都屬于CLR線程池中的工作者線程。
class Program { static void Main(string[] args) { SleepDelegate sleepDelegate = new SleepDelegate(Sleep); IAsyncResult result = sleepDelegate.BeginInvoke("Sleep", null, null); string data = sleepDelegate.EndInvoke(result); Console.WriteLine(data); Console.ReadKey(); } delegate string SleepDelegate(object o); //模擬執(zhí)行長時(shí)間的任務(wù) static string Sleep(object state) { string name = (string)state; ThreadMessage(name); Console.WriteLine("我還沒有執(zhí)行完呢,請耐心等待...."); Thread.Sleep(3000); return "Hello" + name; } static void ThreadMessage(string data) { string message = string.Format("ThreadName is {0} ThreadId is:{1}", data, Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); } } 委托還有個(gè)可以調(diào)用回調(diào)函數(shù)的
ok,就寫到這吧,委托異步處理的異常處理和等待所有線程完成,都可以參照4.4和4.5. 同時(shí),在delegate.EndInvoke() 處也可以catch住異常.(這個(gè)在執(zhí)行exe時(shí)候可看到效果,直接調(diào)試程序要報(bào)錯) 這篇文章,是自己對多線程學(xué)習(xí)和總結(jié)吧. 對于多線程中的,線程安全,不是太了解,可能是下一步要探究的對象吧.
http://blog.csdn.net/wilsonke/article/details/7616984
|
新聞熱點(diǎn)
疑難解答