官术网_书友最值得收藏!

2.5 多線程技術

2.5.1 多線程概述

在網絡編程中創建的應用程序經常涉及一個或者多個線程,因此,對于每個程序員來講,線程(Thread)是必須掌握的知識。線程是操作系統分配處理器時間的基本單元,是系統中可以并行執行的程序段,擁有起點、執行的順序系列和一個終點。一個或多個線程組成一個進程。每個應用程序用單個線程啟動,但在該應用程序域中的代碼可以創建附加線程。

在多線程程序運行過程中,線程主要負責維護自己的堆棧,這些堆棧用于異常處理、優先級調度和其他一些執行程序重新恢復線程時需要的信息。同一進程中的線程可以共享此進程的資源和內存空間。

在多線程應用程序中可以同時執行多個操作。當一個線程必須阻塞時,CPU可以運行其他線程而不是等待。這樣可以大幅提高程序的效率。例如,在瀏覽器中下載圖像時,可以滾動頁面,在訪問新的頁面時播放動畫、聲音及打印文件等。

2.5.2 多線程的創建與使用

C#的System.Threading命名空間提供了大量的類和接口來支持多線程編程。其中,Thread類用于對線程進行管理,包括線程的創建、啟動、終止、合并以及休眠等。Thread類的常用屬性和方法如表2-10所示。

表2-10 Thread類的常用屬性和方法

下面介紹線程的基本操作。

1. 線程的創建和啟動

.NET可以通過以下語句創建并啟動一個新的線程:

     Class1 c1=new Class1();
     Thread thread=new Thread(new ThreadStart(c1.ThreadFunc)); //ThreadFunc為方法名
     thread.Start();

第二條語句中,thread線程對象通過System.Threading.ThreadStart類的一個實例以類型安全的方法調用c1.ThreadFunc方法。一旦方法Start()被調用,該線程將保持“alive”狀態,可以通過它的IsAlive屬性進行查詢。

如果要向線程啟動的方法中傳遞參數,可以將該方法和參數都封裝到一個類里面,參數作為屬性,通過實例化該類,方法就可以調用屬性來實現參數的安全傳遞,如下所示:

當線程啟動的方法僅帶一個參數時,可以在啟動線程時傳遞實參,這種情況下作為線程啟動的方法的參數類型必須是Object類型,如下所示:

     public void ThreadFunc(object name)
     { string s=name as string;
        Console.WriteLine(s);
     }
     Thread thread=new Thread(ThreadFunc);
     thread.Start("帶參數的線程");

2. 前臺線程和后臺線程

.NET的公共語言運行時(CLR)將線程分為前臺線程和后臺線程。應用程序必須運行完所有的前臺線程才能退出;而所有的后臺線程在應用程序退出時都會自動結束,無論它們的工作是否完成。通過線程的IsBackground屬性可以設置一個線程是否是前臺線程。

3. 線程的掛起和重新開始

Thread類的方法Thread.Suspend()可以暫停一個正在運行的線程,而Thread.Resume()則可以讓那個線程繼續執行。.NET框架不記錄線程掛起的次數,即無論線程掛起幾次,只需調用Resume方法一次就可以讓掛起的線程重新運行。

如果希望線程暫停一段時間以便CPU將時間片中剩余部分分配給其他線程,可以調用Thread.Sleep方法。例如:

     Thread.Sleep(1000);

該語句讓當前線程暫停1000ms。

如果參數是0,如:

     Thread.Sleep(0);

則指示應掛起此線程以使其他等待線程能夠執行。

注意,以上這些方法是針對它們所在的線程執行的,而不是其他線程。

4. 終止線程

線程啟動后,如果線程執行的方法運行結束,則線程終止。因此對于長時間運行的服務線程,可以在線程執行的方法中設置一個bool變量,線程執行過程中循環判斷該變量,以確定是否讓方法運行結束,從而退出線程。在其他線程中通過修改該bool變量的值實現對該線程結束的控制。這是結束線程比較好的方法。例如:

另外,也可以通過調用Thread類的Abort方法讓線程強行終止,例如:

     Thread td=new Thread(方法名);
     …
     td.Abort();

由于系統對非正常結束的線程要進行代碼清理等工作,使用Abort方法終止線程時,線程并不一定會立即結束。如果調用Abort方法后系統自動清理工作還在進行,則可能出現類似死機一樣的假象。

5. 合并線程

Join方法用于將指定線程合并到當前線程中。例如,在線程t1的執行過程中需要等待另一個線程t2結束后才能繼續執行,則在t1的代碼中使用Join方法:

     t2.Join();

這樣,當t1中的代碼執行上句后,t1會處于暫停狀態,直到t2執行完才會繼續執行。

如果只是希望t1線程等待一段時間,無論t2是否執行結束t1都繼續執行,則可以使用帶參數的Join方法,例如:

     t2.Join(100);

讓t1等待t2執行100ms后繼續執行。

6. volatile關鍵字

volatile關鍵字表示它修飾的字段可以被多個并發執行的線程修改。對于多線程訪問的字段,如果該字段沒有用lock語句對訪問進行序列化,則該字段應該用volatile修飾。

volatile關鍵字只能用在類或結構體的字段定義中,如修飾上面的bStopped字段,不能將局部變量聲明為volatile。

可被volatile修飾的類型有:

(1)引用類型;

(2)指針類型(在不安全的上下文中);

(3)整型,如sbyte、byte、short、ushort、int、uint、char、float和bool;

(4)具有整數基類型的枚舉類型;

(5)形式類型為引用類型的泛型類型參數;

(6)IntPtr和UIntPtr。

例2-6】 線程基本使用方法演示。

程序運行結果如圖2-17所示。

圖2-17 線程基本使用方法演示結果

例2-7】 用多線程方法改善例2-3中的客戶端/服務器端通信,實現服務器可以與多個客戶端通信,并隨時接收客戶端發送的消息。

(1)服務器端程序,Program類中代碼如下:

(2)改寫客戶端程序,使其分批發送數據。Program類中代碼如下:

7. 在一個線程中訪問另一個線程的控件

默認情況下,.NET Framework不允許在一個線程中直接訪問另一個線程的控件,因為如果多個線程同時訪問某一個控件,會使該控件進入一種不確定的狀態。但是,為了在窗體上顯示線程中的處理消息,我們可能要經常在一個線程中訪問另一個線程的控件。有兩種方法可以實現這個功能,一種是使用委托(Delegate)和事件(Event);另一種是利用BackgroundWorker組件。這里僅介紹使用委托和事件實現的方法。

為了讓不是創建控件的線程訪問該控件對象,Windows應用程序中的每個控件都有一個Invoke方法,該方法利用委托實現使非創建控件的線程對該控件進行操作。具體用法是先查詢控件的InvokeRequired屬性值,如果該值為true,說明訪問該控件的線程不是當前線程,這時需要利用委托訪問控件,否則直接訪問控件。例如,在另一個線程中調用控件的AddText方法,實現對textBox1控件顯示文本的追加:

圖2-18 例2-8的主界面

例2-8】 編寫如圖2-18所示的Windows程序。定義一個CTextOutput類,在該類中定義方法WriteText,用于不停地將主界面編輯框中的文本填寫到主界面的ListBox控件中。同時,每當編輯框內文本發生變化時,新的文本內容自動填寫到ListBox控件中。

(1)新建名為AccessControlInThread的Windows應用程序,界面設計如圖2-18所示。

(2)在“解決方案資源管理器”中,添加名為CTextOutput.cs的文件。將該文件代碼改為如下形式:

(3)切換到Form1.cs的代碼編輯界面,將代碼改為下面內容:

(4)按F5鍵編譯并執行。單擊“啟動線程”按鈕,程序將文本框中的內容填入列表框,每當編輯框中的文本發生變化時,新的文本自動填入列表框;單擊“終止線程”按鈕,填表停止。

2.5.3 多線程的同步

多個線程同時運行時,根據線程之間的邏輯關系決定誰先執行,誰后執行,這就是線程同步。在學習線程同步前,先了解下一線程的優先級。

CPU按照線程的優先級進行線程時間片的分配和服務。C#將線程分為5個不同的優先級。創建線程時,如果不指定優先級,系統默認為Normal。如果要賦予高優先級,可以使用下面方法:

     Thread t=new Thread(MethodName);
     t.priority=ThreadPriority.AboveNormal;

通過線程優先級的改變可以改變線程的執行順序,所設置的優先級僅適用于這些線程所屬的進程。值得注意的是,當某一線程的優先級設置為Highest時,系統上正在運行的其他線程都會停止,所以除非是必須立即處理的任務,否則不使用這個優先級。

多線程處理解決了吞吐量和響應速度的問題,但也帶來了資源共享問題,如死鎖和資源爭用。如果一個線程必須在另一個線程完成某個工作后才能繼續執行,則必須考慮如何讓它們保持同步,以確保系統上同時運行的多個線程不會出現死鎖或邏輯錯誤。

假設A、B兩個線程有相同優先級且同時在同一系統上運行,如果先給A分配時間片,它將在變量var1中寫入某個值,但在尚未執行完線程A時,時間片已經用完,此時時間片已分配給了線程B使用,而B恰好要嘗試讀取var1的值,此時讀出的就是不正確的值,如此便會出錯。這種情況下,若使用線程同步,則可避免錯誤出現,因為同步僅允許一個線程使用相同資源,當其使用結束后才讓其他線程使用。

要解決多線程編程中的同步問題,我們使用得最多的是C#提供的lock語句。lock關鍵字能確保當一個線程位于代碼的臨界區(可理解為一段代碼)時,另一個線程不進入臨界區。如果其他線程試圖進入鎖定的代碼段,則它將被阻塞,直到鎖定的對象被釋放。

lock關鍵字將代碼段(語句塊)標記為臨界區,其原理為首先鎖定一個私有對象,然后執行代碼段中的語句,當代碼段中的語句執行完畢后,再解除該鎖。使用形式如下:

     private Object obj=new Object();
     ...
     lock(obj)
     {
     ...
     }

注意,鎖定的對象名(上面的obj)一般聲明為Object類型,不聲明為值類型。而且一定要將其聲明為private,不能為public,否則lock語句無法控制,易產生死鎖等一系列問題。另外,臨界區的代碼不宜過多。由于鎖定一個對象后,在解鎖該對象之前,其他任何線程都不能執行lock語句所包含的代碼塊中的內容,因此,在臨界區中代碼過多會降低應用程序的性能。

2.5.4 線程池的概念與使用方法

線程池是在后臺執行多個任務的線程集合。一般在服務器端應用程序中使用線程池接收客戶端的請求,每個傳入的請求都會被分配一個線程,從而達到異步處理請求的目的。

但是,在服務器應用程序中,若每收到一個請求就創建一個新線程,將不可避免地增大系統開銷,甚至可能會導致由于過度地使用資源而耗盡內存。為了防止資源不足,服務器端應用程序可以采用線程池來限制同一時刻處理線程的數目,即最大線程數限制。如果線程池滿了,則進入線程池的線程需等待線程池中有空余線程可分配時才可進入。

System.Threading命名空間提供了一個ThreadPool類對線程池進行操作。其語法形式為:

     public static class ThreadPool

由上可知,ThreadPool是一個靜態類。該類只提供了一些靜態方法,不能創建該類的實例。注意,托管線程池中的線程為后臺線程,這意味著當所有前臺線程退出后ThreadPool也會退出。

每個進程有一個線程池。線程池默認大小為25個線程。使用SetMaxThreads方法可以更改線程池的線程數。每個線程使用默認的棧堆大小并按照默認的優先級運行。

表2-11列出了ThreadPool類的常用方法。

表2-11 ThreadPool類的常用方法

請求線程池處理一個任務或者工作項可以調用QueueUserWorkItem方法。這個方法帶有一個WaitCallback委托參數,該參數包裝了要完成的任務。運行時線程池會自動為每一個任務創建線程并在任務釋放時釋放線程。

下面代碼說明了如何創建線程池和添加任務:

線程池適用于需要多個線程而實際執行時間不多的場合,例如有些經常處于等待狀態的線程。線程池技術非常適合于服務器程序接收大量的短小線程請求的情況,它可以大大減少創建和銷毀線程的次數,從而提高工作效率。若線程要求運行時間比較長,則僅靠減少線程的創建時間對系統效率的提高就不明顯,此時就不能僅依靠線程池技術,而需要借助其他技術來提高服務效率了。

主站蜘蛛池模板: 盐边县| 七台河市| 海城市| 屏东县| 内乡县| 苗栗市| 河源市| 城固县| 淳化县| 科尔| 开阳县| 扬州市| 芜湖县| 彰武县| 离岛区| 恩平市| 巢湖市| 达孜县| 达日县| 丰顺县| 井冈山市| 墨江| 南开区| 工布江达县| 晋州市| 青阳县| 陆丰市| 吉木乃县| 宣恩县| 大姚县| 万年县| 本溪市| 辽源市| 辰溪县| 聂拉木县| 五寨县| 郴州市| 琼结县| 潼关县| 固阳县| 富裕县|