C#語言 線程(一)線程基礎(1)

0 概述

Windows系列操作系統是建立在保護模式之上的32位/64位多任務操作系統,其特點是:時分搶先式多任務操作系統。我們來詳細探討一下其中的定義。

在操作系統中,進程線程是和我們運行程序緊密相關的兩個概念,其中:

  • 進程是資源分配單元,用於執行一段程序前爲其分配足夠的資源;
  • 線程是程序執行單元,線程用於執行程序。


簡單的敘述一下Windows操作系統是如何來啓動一個應用程序(.exe文件)的。

  1. 操作系統分配一個進程,並向CPU下達一系列指令,包括創建該進程的虛擬內存映射表(關於虛擬內存,請參閱操作系統原理相關書籍,32位保護模式這一概念),分配虛擬內存,設定進程描述符等等,最終將進程封閉到一個獨立的虛擬內存地址空間中,返回進程句柄;
  2. 操作系統爲該進程創建一個線程,並啓動它;
  3. 該線程從應用程序的Main方法開始執行。


也即是說,所有的Windows進程,都缺省擁有一個線程,這個線程稱爲主線程;主線程可以根據代碼要求創建其它線程,稱爲輔助線程

 

舉個形象的比喻,計算機是一個工廠,操作系統是這個工廠的領導者,進程是工廠的車間,線程是工廠的工人。工廠要能夠正常運轉,需要有領導者統一調配資源,把工作任務分配給每個車間,由派到該車間的工人進行工作。一個正在生產的車間必然得有一名工人,但該工人可以根據工作的需要,隨時要求其他工人一起來共同工作。

在計算機中,CPU一般只有一個或幾個,而同時運行的線程(一個或多個進程中)卻有多個,其數量遠遠大於CPU的數量,那麼,操作系統是如何在有限的CPU上調度這麼多線程呢?

 

圖1 線程調度示意圖

 

如上圖所示,Windows操作系統採用了“時分”的概念來調度多個線程,即每個線程佔用CPU一段時間後,就會被強行將執行權交給下一個線程。線程輪流來使用CPU。使用CPU的線程,正常執行程序,時間到達時,操作系統執行線程切換操作:將CPU寄存器的內容全部轉移到內存中,將另一個線程保存的寄存器狀態從內存恢復到寄存器中,由於寄存器中保存了某個線程正在運行哪一行代碼的指針,所以切換後另一個線程可以繼續上一次運行狀態繼續運行。這種切換線程的方式稱爲:現場保存現場恢復

線程的時分調度,其實也不是將CPU時間平均分配給各個線程,因爲不同線程執行的任務總有個輕重緩急之分,所以Windows由採用了一種叫做“搶先式”的方式來進一步調度線程。

在Windows操作系統中,每個進程都有一個優先級,進程中的線程也有一個優先級,優先級越高的進程,操作系統會分配給其線程更多的CPU時間,進程中哪個線程的優先級更高,操作系統也會分配給其更多的CPU事件。

總之一句話:讓所有的線程都有機會運行,但優先級高的線程運行的時間更久一些。這就叫做時分搶先式多任務。

前面我們所做的練習,代碼都是在一個線程中運行,現在我們知道這個線程叫做“主線程”。後面的課程,我們要學習如何建立輔助線程,在一個進程中調度多個線程。

1 建立並啓動線程

和主線程一樣,輔助線程也有一個入口方法。主線程的入口方法是Main方法,輔助線程的入口方法可以隨意指定,可以是任意對象的符合ParameterizedThreadStart委託類型的任意方法。

ParameterizedThreadStart委託聲明如下:

public delegate void ParameterizedThreadStart(object arg)

即任意返回類型爲void,具備一個object類型參數的方法,都可以作爲線程的入口方法。

Thread類(System.Threading.Thread類)是.net Framework提供的線程操作封裝類,在實例化Thread類對象時指定線程入口方法,然後調用Thread類對象的Start方法啓動線程即可。

{
    private void TheadWork(object arg) { 
        // 線程執行代碼 
    } 
     
    static void Main(string[] args) { 
        Thread thread = new Thread(new ParameterizedThreadStart(this.ThreadWork)); 
        thread.Start(); 
    }
}

當Thread類型的Start方法被執行後,ThreadWork方法中的代碼就開始執行。

對於在主線程(例如Main方法中)裏直接調用某個方法的情況,我們稱爲方法的同步調用,即直到被調用方法執行完畢返回,其後的代碼才能夠被執行;對於在主線程(例如Main方法中)裏通過Thread類的Start方法調用某個方法,我們稱爲方法的異步調用,即被調用方法和主線程中其後代碼是同時執行的。

還可以寫的簡單點,對於不需要進一步操作Thread對象的情況下(大部分時候都不需要的),也可以不聲明Thread類型的變量,一步到位

{
    private void TheadWork(object arg) { 
        // 線程執行代碼 
    } 
     
    static void Main(string[] args) { 
        new Thread(new ParameterizedThreadStart(this.ThreadWork)).Start(); 
    } 
}

下面我們看一個完整的例子:

建立一個窗體FormMain如下:

image

 
圖2 主界面

FormMain.cs文件

1   using System;
2   using System.Threading;
3   using System.Windows.Forms;
4    
5   namespace Edu.Study.Multithreading.Basic {
6    
7       /// <summary>
8       /// 線程中操作文本框的委託類型
9       /// </summary>
10       /// <param name="textBox">要操作的文本框</param>
11       /// <param name="num">文本框中顯示的數字</param>
12       public delegate void SetTextBoxHandler(TextBox textBox, int num);
13    
14    
15       /// <summary>
16       /// 主窗體類
17       /// </summary>
18       public partial class FormMain : Form {
19    
20           // 聲明兩個線程變量
21           private Thread thread1, thread2;
22    
23           /// <summary>
24           /// 構造器
25           /// </summary>
26           public FormMain() {
27               InitializeComponent();
28    
29               // 實例化線程1對象
30               this.thread1 = new Thread(new ParameterizedThreadStart(this.ThreadWork));
31    
32               // 實例化線程2對象
33               this.thread2 = new Thread(new ParameterizedThreadStart(this.ThreadWork));
34           }
35    
36           /// <summary>
37           /// 線程工作方法(線程入口點方法)
38           /// </summary>
39           /// <param name="arg">線程參數</param>
40           public void ThreadWork(object arg) {
41               int num = 1;
42               try {
43                   // 無限循環, 這種代碼一般禁止寫在主線程中, 那樣會導致程序失去響應, 但在輔助線程中則可以
44                   while (true) {
45    
46                       // 使用窗體的Invoke方法執行委託, 該委託對應的方法(SetTextBox方法)將在主線程中被執行
47                       // 在輔助線程中, 無法直接操作主線程創建的窗體, 所以要使用委託
48                       this.Invoke(new SetTextBoxHandler(this.SetTextBox), arg, num++);
49    
50                       // 線程休眠(暫停)10ms
51                       Thread.Sleep(10);
52                   }
53               } catch (ThreadAbortException) {
54                   MessageBox.Show("線程已結束");
55               }
56           }
57    
58           /// <summary>
59           /// 在線程中訪問窗體或控件的委託方法, 符合委託類型SetTextBoxHandler
60           /// </summary>
61           /// <param name="textBox">要操作的文本框</param>
62           /// <param name="num">文本框中顯示的數字</param>
63           private void SetTextBox(TextBox textBox, int num) {
64               textBox.Text = num.ToString();
65           }
66    
67           /// <summary>
68           /// 啓動按鈕點擊事件
69           /// </summary>
70           private void startButton_Click(object sender, EventArgs e) {
71               // 啓動線程1, 爲線程入口方法傳入參數firstTextBox對象
72               this.thread1.Start(this.firstTextBox);
73    
74               // 啓動線程2, 爲線程入口方法傳入參數secondTextBox對象
75               this.thread2.Start(this.secondTextBox);
76    
77               // 禁用啓動按鈕, 防止線程被重複啓動
78               this.startButton.Enabled = false;
79           }
80    
81           /// <summary>
82           /// 窗體關閉事件
83           /// </summary>
84           private void FormMain_FormClosing(object sender, FormClosingEventArgs e) {
85    
86               // Abort方法用於立即結束線程運行
87               this.thread1.Abort();
88               this.thread2.Abort();
89           }
90       }
91   }

Program.cs文件

1   using System;
2   using System.Windows.Forms;
3    
4   namespace Edu.Study.Multithreading.Basic {
5       static class Program {
6           /// <summary>
7           /// 應用程序的主入口點。
8           /// </summary>
9           [STAThread]
10           static void Main() {
11               Application.EnableVisualStyles();
12               Application.SetCompatibleTextRenderingDefault(false);
13               Application.Run(new FormMain());
14           }
15       }
16   }

代碼很簡單,有幾點需要說明一下:

第30-33行,在FormMain的構造器中,實例化的線程對象,此時只是獲取到了線程對象的引用並指定了線程入口方法,線程並未啓動;
第72-75行,在startButon按鈕的點擊事件處理方法中,使用Thread類的Start方法將兩個線程啓動起來。線程對象thread1和thread2都指定當前類的ThreadWork方法爲入口方法,所以相當於調用了ThreadWork方法兩次,只不過是異步調用的。此時主線程和兩個輔助線程同時運行。這裏給線程對象的Start方法設置了參數,該參數會作爲arg參數傳入線程入口方法(40行);
第77行,將啓動線程的按鈕設置爲禁用,放置再次啓動線程——一個線程對象只能啓動一個線程;
第40-56行,線程入口方法代碼,該方法被兩個線程同時執行了兩次,各自將一個數字從1開始不斷加1。
第44行,一般而言,由於主線程需要處理窗口消息,所以在主線程中執行無限循環會導致窗口失去響應(主線程陷入死循環,無法運行處理窗口消息的代碼,這樣會導致界面失去響應),但在輔助線程中執行則可以執行這一類無限循環代碼(輔助線程運行的同時,不影響主線程運行處理窗口消息的代碼),所以使用異步方法來執行費時的工作,讓主線程空閒下來,這是提高界面響應能力的主要方法;
第51行,Thread.Sleep方法是讓執行這一方法的線程休眠一段時間,在本例中,休眠的作用就是人爲的降低當前線程佔用CPU的時間。由於這種死循環相當佔用CPU,有可能會導致其它線程得不到必要的CPU運行時間,Sleep可以初步解決這個問題;
第48行,Control類(這裏被Form類繼承)的Invoke方法的作用是這樣的:將一個方法的調用插入到窗體的調用隊列中,該隊列由主線程維護。即插入調用隊列的方法會在稍候被主線程執行。由於窗體部分的代碼並不是爲多線程環境設計的(沒有這個必要),所以當我們在一個線程中調用其它線程創建的窗體或其控件的屬性和方法時,會引發線程不安全操作(即後面講到的同步問題),拋出異常;解決方法就是通過窗體對象的Invoke方法將要調用的窗體對象的操作通過委託委託給主線程調用,即不存在任何問題。委託類型可以隨意定義(本例定義了一個返回類型爲void,具備一個TextBox類型和一個int類型參數的委託類型,第12行)。委託方法中,用方法的第二個整數參數給第一個文本框類型參數的Text屬性賦值(第63-65行)。Invoke方法的第一個參數爲委託方法,其後的參數爲調用該委託方法的實參;
第87-88行,Thread對象的Abord方法用於在一個線程中停止另一個線程的執行,此時該線程立即拋出一個ThreadAbortException異常,通過拋出異常跳出線程代碼執行。通過try…catch(ThreadAbortException)…段包圍線程方法內的代碼,可以在線程被中止後得到一個通知(第42、53行);
執行上述例子,將會看到兩個文本框在同時刷新數字,關閉窗口後即可停止線程執行。

image

 圖3 執行結果

 

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/mousebaby808/archive/2010/04/08/5465225.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章