一、定義與理解
1、定義
線程是操作系統分配CPU時間片的基本單位,每個運行的引用程序為一個進程,這個進程可以包含一個或多個線程。
線程是進程中的執行流程,每個線程可以得到一小段程序的執行時間,在單核處理器中,由於切換線程速度很快因此感覺像是線程同時允許,其實任意時刻都只有一個線程運行,但是在多核處理器中,可以實現混合時間片和真實的並發執行。但是由於操作系統自己的服務或者其他應用程序執行,也不能保證一個進程中的多個線程同時運行。
線程被一個CLR委托給操作系統的進程協調函數管理,確保所有線程都可以被分配適當的執行時間,同時保證在等待或阻止的線程不占用執行時間。
2、理解
線程與進程的關鍵區別是:進程是彼此隔離的,進程是操作系統分配資源的基本單位,而同一個進程中的多個線程是共享該進程內存堆區(Heap)的數據的,可以進行直接的數據共享。但是對於同一進程內的不同線程維護各自的內存棧(Stack),因此各線程的局部變量是隔離的。通過下面的例子可以看出。
- static void Main(string[] args)
- {
- Thread t = new Thread(Write);
- t.Start();
- Write();
- Console.ReadKey();
- }
- static void Write()
- {
- for (int i = 0; i < 5; i++)
- Console.Write("@");
- }
結果輸出的是10個“@”,在兩個線程中都有局部變量i,是彼此隔離的。但是對於共享的引用變量和靜態數據,多個線程是會產生不可預知的結果的,這里共享的數據也就是“臨界數據”,從而引發了線程安全的概念。
- static bool done;
- static void Main(string[] args)
- {
- Thread t = new Thread(Write);
- t.Start();
- Write();
- Console.ReadKey();
- }
- static void Write()
- {
- if (!done)
- {
- done = true;
- Console.Write("@");
- }
- }
這里輸出的只有一個字符,但是很可能在極少數情況下會出現輸出兩個字符的情況,而且這是不可預知的。但是,對於共享的引用就不會出現這種情況。
二、線程使用情形
- 客戶端應用程序保持對用戶的響應:由於某些應用程序的特定需求,多線程程序一般用來執行需要非常耗時的操作,此時使用主線程創建工作線程在后台執行耗時的任務,而主線程保持運行,例如保持與用戶的交互(更新進度條、顯示提示文字等),這樣可以防止由於程序耗時而被操作系統提示“無響應”而被用戶強制關閉進程。
- 及時處理請求:對於Web應用程序,主線程相應客戶端用戶的請求,返回數據的同時,工作線程從數據庫選出最新數據。這樣可以對某些實時性要求高的應用非常有效,同時可以查詢工作量被單獨線程分開執行,特別是在多核處理器上,可以提高程序的性能。同時對於服務器需要處理多種類型的請求的時候,如ASP.NET、WCF、Remoting等,從而可以實現並發響應。
- 防止一個線程長時間沒有響應而阻塞CPU來提高效率:例如WebService服務,對於沒有用戶交互界面的訪問,在等待提供webservice服務(比較耗時)的電腦的響應的同時可以執行其他工作,以提高效率。
問題:
多線程的問題是使程序中的多個線程的交互變得過於復雜,會帶來較長的開發時間和間歇性或非重復性的bug。同時線程數目不能太多,否則頻繁的分配和切換線程會帶來資源和CPU的開銷,一般有一個到兩個工作線程就足夠。
三、C#中的線程
C#中主要使用Thread類進行線程操作,位於System.Threading命名空間下,提供了一系列進行多線程編程的類和接口,有線程同步和數據訪問的Mutex、Monitor、Interlocked和AutoResetEvent類,以及ThreadPool類和Timer類等。
首先使用new Thread()創建出新的線程,然后調用Start方法使得線程進入就緒狀態,得到系統資源后就執行,在執行過程中可能有等待、休眠、死亡和阻塞四種狀態。正常執行結束時間片后返回到就緒狀態。如果調用Suspend方法會進入等待狀態,調用Sleep或者遇到進程同步使用的鎖機制而休眠等待。具體過程如下圖所示:
Thread類主要用來創建並控制線程,設置線程的狀態、優先級等。創建線程的時候使用ThreadStart委托或者ParameterizedThreadStart委托來執行線程所關聯的部分代碼(也就是工作線程的運行代碼)。
屬性 | 說明 |
---|---|
CurrentThread | 獲取當前正在運行的線程 |
IsAlive | 獲取當前線程的執行狀態 |
Name | 獲取或設置線程的名稱 |
Priority | 獲取或設置線程的優先級 |
ThreadState | 獲取包含當前線程狀態的值 |
方法 | 說明 |
---|---|
Abort | 調用此方法的線程引發ThreadAbortException 終止線程 |
Join | 阻止調用線程,知道某個線程終止時為止 |
Resume | 繼續已掛起的線程 |
Sleep | 將線程阻止指定的毫秒數 |
Start | 將線程安排被進行執行 |
Suspent | 掛起線程,如果已經掛起則不起作用 |
四、創建與運行設置
1、創建
使用Thread類的構造函數創建線程的時候,需要傳遞一個新線程開始執行的代碼塊,提供了使用無參數的TheadStart委托和帶有一個參數的ParameterizedTheadStart委托。他們的定義如下:
- public delegate void ThreadStart();
- public delegate void ParameterizedThreadStart(object obj);
任何時候C#使用上述兩個委托中的一個自動進行線程的創建。
- static void Main()
- {
- Thread t = new Thread(new TheadStart(Go));
- t.Start();
- Go();
- }
- static void Go()
- {
- Console.Write("hello!");
- }
上述方式不傳遞參數,可以使用new Thead(Go)的方式直接創建,此時C#會在編譯時自動匹配使用的是ThreadStart委托創建的。下面可以進行傳遞參數創建線程。
- static void Main()
- {
- Thread t = new Thread(Go);
- t.Start("hello");
- Go();
- }
- static void Go(object msg)
- {
- string message = (string)msg;
- Console.Write(message);
- }
此時實際在編譯時使用的new Thread(new ParameterizedThreadStart(Go("hello")))創建的,上述使用Start方法傳遞的參數會默認采用這種方式構建。
第二種方法是使用Lambda表達式:
- new Thread( () => Go("hello") );
第三種方法是使用匿名方法:
- new Thread( () => {
- Console.Write("hello world!");
- ......
- }).Start();
注意問題:使用Lambda表達式的時候會存在變量捕獲的問題,如果捕獲的變量是共享的,會出現線程不安全的問題。看下面的例子:
- static void Main(string[] args)
- {
- for (int i = 0; i < 10; i++)
- new Thread(() => Write(i)).Start();
- Console.ReadKey();
- }
- static void Write(object obj)
- {
- string msg = Convert.ToString(obj);
- Console.Write(msg);
- }
上述由於使用Lambda表達式傳遞參數,在for循環的作用域內,新建的十個線程共享了局部變量i,傳遞進入i參數可能被多個線程已經修改,因此每次輸出結果都是不確定的,兩次結果如下:
上述問題,可以使用在循環體內使用一個tmp變量保存每次的變量i值,這樣輸出的就是0到9這十個數。因為使用tmp變量之后的代碼可以用下面的來理解:
- int i = 0;
- int tmp = i;
- new Thread(()=>Write(tmp)).Start();
- int i = 1;
- int tmp = i;
- new Thread(()=>Write(tmp)).Start();
- ...
上述使用Lambda表達式傳遞參數的問題,使用Start方法傳遞參數也會出現這樣的線程不安全的問題,需要使用特殊的線程同步手段進行避免。
2、設置
通過使用Thread.CurrentThread屬性獲取正在運行的線程對象。每個線程都有一個Name屬性,可以設置和修改,但是只能設置一次。這樣可在調試窗口看到每個線程的工作狀態,便於調試。
線程有前台和后台之分,可以使用IsBackground屬性設置,但是這個屬性與線程的優先級是沒有關聯的。前台線程只要有一個在運行應用程序就在運行,當沒有前台線程運行后應用程序終止,也就是在任務管理器中的程序一欄中沒有了此程序,但是此時后台線程任然運行直到其完成操作結束,因此在任務管理器的進程一欄中會找到。
- static void Main(string[] args)
- {
- Thread.CurrentThread.Name = "main";
- Thread t = new Thread(Go);
- t.Name = "worker";
- t.Start();
- Go();
- Console.ReadKey();
- }
- static void Go()
- {
- Console.WriteLine("from " + Thread.CurrentThread.Name);
- Console.WriteLine("background status: " + Thread.CurrentThread.IsBackground.ToString());
- }
前台或主線程明確等待任何后台線程完成后再結束才是最好的方式,這大多使用Join方式實現,如果某個工作線程無法實現,可以先終止它,如果失敗再拋棄線程,從而與進程一起消亡。
線程的優先級使用Priority設置或獲取,只有在運行時才有作用。分為5個級別:
- enum ThreadPriority{Lowest, BelowNormal , Normal, AboveNormal, Highest}
線程優先級設置高並不意味着能執行實時的工作,這受限於所屬進程的級別,要執行實時的工作需要提示System.Diagnostics命名空間下的Process級別:
- using (Process p = Process.GetCurrentProcess())
- p.PriorityClass = ProcessPriorityClass.High;
設置為High是一個短暫的最高優先級別,如果設置為Realtime,那么將讓操作系統不然該進程被其他進程搶占,因此如果此程序一旦出現故障將耗盡操作系統資源。因此設置為High就是被認為最高和最有用的進程級別了。
對於有用戶界面的程序不適合提升進程級別,因為界面UI的更新需要耗費CPU很多時間,從而拖慢電腦。最好的方式是實時工作和用戶界面使用不同的進程,有不同的進程優先級,通過Remoting或者共享內存的方式進行進程通信。
線程執行先運行最高優先級的線程,高優先級的線程執行完之后才開始執行低優先級的線程。
3、休眠
Thread.Sleep(int ms); Thread.Sleep(TimeSpan timeout);
上述方法為Thread類的兩個靜態方法,用來阻止當前線程指定的時間。
4、終止
使用Abort和Join兩個方法實現。Join會等待另一個線程執行完后再執行。而Abort會引發ThreadAbortException異常,同時可以傳遞一個終止的參數信息。
Thread.Abort();或者Thread.Abort(Object stateInfo)。
5、異常處理
每個線程都有獨立的執行路徑,因此放在try/catch/finally塊中的新線程都與之無關。補救的方式是在每個線程處理的方法中加入自己的異常處理機制。
- static void Main(string[] args)
- {
- try
- {
- new Thread(Go).Start();
- }
- catch (Exception ex)
- {
- Console.Write(ex.Message);
- }
- Console.ReadKey();
- }
- static void Go()
- {
- try
- {
- throw null;
- }catch(Exception e){
- Console.Write(e.Message);
- }
- }
上述處理過程在單獨的線程運行中進行異常處理是可以被捕獲到的。同時任何線程內的未處理的異常都會導致整個程序關閉,對於WPF和WinForm程序中的全局異常僅僅在主界面線程執行,對於工作線程的異常需要手動處理。有三種情況可以不用處理工作線程的異常:異步委托、BackgroundWroker、Task Parallel Library。
(后續繼續探秘)