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