14多线程程序设计
多線程程序設計
多線程程序設計
如果在一個程序中,有多個工作要同時做,可以采用多線程。在Windows操作系統中可以運行多個程序,把一個運行的程序叫做一個進程。一個進程又可以有多個線程,每個線程輪流占用CPU的運行時間,Windows操作系統將時間分為時間片,一個線程使用一個時間片后,操作系統將此線程掛起,將另一個線程喚醒,使其使用下一個時間片,操作系統不斷的把線程掛起,喚醒,再掛起,再喚醒,如此反復,由于現在CPU的速度比較快,給人的感覺象是多個線程同時執行。Windows操作系統中有很多這樣的例子,例如復制文件時,一方面在進行磁盤的讀寫操作,同時一張紙不停的從一個文件夾飄到另一個文件夾,這個飄的動作實際上是一段動畫,兩個動作是在不同線程中完成的,也就是說兩個動作是同時完成的。又如Word程序中的拼寫檢查也是在另一個線程中完成的。每個進程最少有一個線程,叫主線程,是進程自動創建的,每進程可以創建多個線程。
不同語言和操作系統對線程提供了不同支持,編寫多線程應用程序的方法也不盡相同。例如,VB6沒有提供對線程的支持,程序員不能處理自己的線程。VC++6.0開發人員必須充分理解Windows線程和處理模型的復雜性,同時擁有這種線程模型的強大功能。
.Net Framework提供了一個完整而且功能強大的線程模型,該模型允許編程人員精確控制線程中運行的內容,線程何時退出,以及它將訪問多少數據等,但使用比VC++6.0簡單。
7.1?? 線程類(Thread)的屬性和方法
線程類在命名空間System.Threading中定義的,因此如果要創建多線程,必須引入命名空間System.Threading。Thread類的常用屬性和方法如下:
l? 屬性Priority:設置線程優先級,有5種優先級類別:AboveNormal(稍高)、BelowNormal(稍低)、Normal(中等,默認值)、Highest(最高)和Lowest(最低)。例如語句myThread.Priority=ThreadPriority.Highest設置線程myThread的優先級為最高。一個線程的優先權并不是越高越好,應考慮到整個進程中所有線程以及其他進程的情況做出最優選擇。優先級相同的線程按照時間片輪流運行。優先級高的線程先運行,只有優先級高的線程停止、休眠或暫停時,低優先級的線程才能運行。
l? 構造函數:Thread(new ThreadStart(線程中要執行的方法名)),構造函數參數中指定的方法需要程序員自己定義,這個方法完成線程所要完成的任務,退出該方法,線程結束。該方法必須為公有void類型的方法,不能有參數。
l? 方法Start():建立線程類對象后,線程處于未啟動狀態,這個方法使線程改變為就緒狀態,如果能獲的CPU的運行時間,線程變為運行狀態。
l? 方法IsAlive():判斷線程對象是否存在,=true,線程存在。
l? 方法Abort():撤銷線程對象。不能撤銷一個已不存在的線程對象,因此在撤銷一個線程對象前,必須用方法IsAlive()判斷線程對象是否存在。
l? 靜態方法Sleep():線程休眠參數指定的時間,單位為毫秒,此時線程處于休眠狀態。線程休眠后,允許其它就緒線程運行。休眠指定時間后,線程變為就緒狀態。
l? 方法Suspend():該方法使線程變為掛起狀態。必須用Resume()方法喚醒掛起線程。
l? 方法Resume():該方法使掛起線程變為就緒狀態,如果能獲的CPU的運行時間,線程變為運行狀態。如果線程多次被掛起,僅調用一次Resume()方法就可以把線程喚醒。
7.2?? 創建線程
例子e7_2:本例使用線程類Thread直接創建一個新的線程,在標簽控件中顯示該線程運行的時間。在窗體放置4個按鈕,單擊按鈕完成新建、掛起、恢復和停止線程的功能。
(1)??? 新建項目。在窗體中放置4個按鈕和一個標簽控件,屬性Name分別為:button1、button2、button3、button4和label1,按鈕屬性Text分別為:新線程、掛起、恢復、撤銷。button1屬性Enabled=true,其余按鈕的屬性Enabled=false。
(2)??? 在Form1.cs頭部增加語句:using System.Threading。
(3)??? 為Form1類定義一個線程類變量:private Thread thread;
(4)??? 為標題為"新線程"的按鈕(button1)增加單擊事件處理函數如下:
private void button1_Click(object sender, System.EventArgs e)
{?? thread=new Thread(new ThreadStart(fun));//生成線程類對象,fun為自定義方法
label1.Text="0";//運行時間從0開始
thread.Start();//線程變為就緒狀態,如能獲的CPU運行時間,線程變為運行狀態
button1.Enabled=false;//標題為"新線程"的按鈕,創建線程后,不允許再創建線程
button2.Enabled=true;//標題為"掛起"的按鈕,允許對運行狀態的線程掛起
button3.Enabled=false;//標題為"恢復"的按鈕,線程未掛起,不能恢復
button4.Enabled=true;// 標題為"撤銷"的按鈕,允許對運行狀態的線程撤銷
}
(5)??? 為標題為"掛起"的按鈕(button2)增加單擊事件處理函數如下:
private void button2_Click(object sender, System.EventArgs e)
{?? thread. Suspend();//線程暫停(掛起)
button1.Enabled=false;
button2.Enabled=false;
button3.Enabled=true;
button4.Enabled=false;
}
(6)??? 為標題為"恢復"的按鈕(button3)增加單擊事件處理函數如下:
private void button3_Click(object sender, System.EventArgs e)
{?? thread. Resume();//暫停(掛起)線程恢復運行
button1.Enabled=false;
button2.Enabled=true;
button3.Enabled=false;
button4.Enabled=true;
}
(7)??? 為標題為"撤銷"的按鈕(button4)增加單擊事件處理函數如下:
private void button4_Click(object sender, System.EventArgs e)
{?? if(thread.IsAlive)
{?? thread.Abort();//撤銷線程對象
button1.Enabled=true;
button2.Enabled=false;
button3.Enabled=false;
button4.Enabled=false;
}
}
(8)??? C#線程模型允許將任何一個void類型的公有方法(靜態或非靜態)作為線程方法,因此允許在任何一個類(不要求這個類是某個類的子類)中定義線程方法,而且同一個類中可以定義多個線程方法。為Form1類定義一個線程方法如下:
public void fun()//在線程中執行的方法,必須為公有void類型方法,不能有參數。
{?? while(true)//退出該方法,線程結束,這里是死循環,線程將一直運行
{?? int x=Convert.ToInt32(label1.Text);
x++;
label1.Text=Convert.ToString(x);
Thread.Sleep(1000);//線程休眠1秒鐘,休眠一次,線程運行了1秒鐘
}
}
(9)??? 在關閉程序之前,必須撤銷線程對象。為主窗體的Closing事件增加事件處理函數如下:
private void Form1_Closing(object sender,System.ComponentModel.CancelEventArgs e)
{?? if(thread.IsAlive)
? ? ? ? ? ? ? ? ? ? ? ? thread.Abort();
}
(10) 編譯,運行,單擊標題為"新線程"的按鈕,新線程開始,計數器從0開始計數。單擊標題為"掛起"的按鈕,線程暫停,計數器也暫停。單擊標題為"恢復"的按鈕,線程重新啟動,計數器也繼續計數。單擊標題為"撤銷"的按鈕,線程對象被撤銷,線程對象不存在,計數器停止計數。運行效果如右圖。
7.3?? 建立線程類
有時需要建立多個線程,每個線程要實現的功能基本相同,但有個別參數不同,例如,每個線程完成同樣的任務,但控制的對象不同。使用線程類Thread直接創建新線程,線程類構造函數參數為一個方法,在這個方法中實現線程所要求的任務,但該方法不能有參數,因此無法通過方法的參數傳遞不同設置。為解決這個問題,可以定義一個自己的線程類。具體實現方法見下例。下邊的例子用到了進度條(ProgressBar)控件,首先介紹進度條控件。
7.3.1? 進度條(ProgressBar)控件
進度條(ProgressBar)控件經常用來顯示一個任務的進度。有時,要完成一個長時間的任務,例如一個軟件的安裝,如果沒有任何提示,使用者可能分不清任務是在進行中,還是死機了,可以使用進度條顯示安裝進度,表示安裝正在進行。進度條常用屬性如下:
l? 屬性Maximum:進度條所代表的最大值(整數),默認值100。
l? 屬性Minimum:進度條所代表的最小值(整數),默認值0。
l? 屬性Step:變化的步長,默認值為10。
l? 屬性Value:進度條當前位置代表的值。修改該值,達到一個Step,進度增加一格。
7.3.2? 用線程控制進度條
例子e7_3_2:建立兩個線程,分別控制兩個進度條(ProgressBar)控件,每個進度條的屬性Value變化的速率不一樣。具體實現步驟如下,運行效果如下圖。
(1)?? 新建項目。在Form1.cs頭部增加語句:using System.Threading。
(2)?? 在窗體中放置2個進度條(ProgressBar)控件。屬性Name分別為progressBar1、progressBar2。
(3)?? 在Form1.cs文件e7_3_2命名空間中,Form1類定義的后邊,建立線程類如下:
public class myThread
{???? private int SleepTime;//線程的休眠時間,從構造函數賦值
private ProgressBar progressBar;//本線程控制哪個進度條,從構造函數賦值
private Thread thread1;
public myThread(int Time,ProgressBar p1)//構造函數,
{???? SleepTime=Time;
progressBar=p1;
thread1=new Thread(new ThreadStart(fun));
thread1.Start();
}
public void fun()//在線程中執行的方法,必須為公有void類型方法,不能有參數。
{?? while(progressBar.Value!=100)
{?? progressBar.Value+=1;
Thread.Sleep(SleepTime);
}
}//退出該方法,線程結束
}
(4)?? 為Form1類增加變量:myThread? myThread1,myThread2。
(5)?? 為Form1類構造函數增加語句如下:
myThread1=new myThread(100,progressBar1);
myThread2=new myThread(200,progressBar2);
(6)?? 編譯,運行,可以看到兩個進度條以不同的速度前進,當進度條被添滿,線程停止。
7.4?? 多個線程互斥
多個線程同時修改共享數據可能發生錯誤。假設2個線程分別監視2個入口進入的人數,每當有人通過入口,線程用C#語句對總人數變量執行加1操作。一條C#語句可能包含若干機器語言語句,假設C#語句加1操作包含的機器語言語句是:先取總人數,加1,再存回總人數。操作系統可以在一條機器語言語句結束后,掛起運行的線程。如當前總人數為5,線程1運行,監視到有人通過入口,取出總人數(此時為5)后,線程1時間用完掛起。線程2喚醒,也監視到有人通過入口,并完成了總人數加1并送回的操作,總人數為6,線程2掛起。線程1喚醒,對已取出的總人數(此時為5)加1,存回總人數,總人數應為7,實際為6,少算一個。為了防止此類錯誤,在一個線程修改共享資源(例如上例的總人數變量)時,不允許其它線程對同一共享資源進行修改,這叫線程的互斥。這樣的實例很多,例如計算機中的許多外設,網絡中的打印機等都是共享資源,只允許一個進程或線程使用。
7.4.1? 多個線程同時修改共享數據可能發生錯誤
例子e7_4_1:下邊的例子模擬2個線程同時修改同一個共享數據時可能發生的錯誤。
(1)?? 新建項目。在Form1.cs頭部增加語句:using System.Threading。
(2)?? 在窗體中放置一個標簽控件,屬性Name=label1。
(3)?? 為Form1類定義2個線程類變量:Thread thread1,thread2。定義整形變量:int num=0。
(4)?? 為Form1類構造函數增加語句如下:
thread1= new Thread(new ThreadStart(Fun1));
thread2= new Thread(new ThreadStart(Fun2));
thread1.Start();
thread2.Start();
(5)?? 為Form1類中定義Fun1()和Fun2()方法如下:
public void Fun1()//在線程中執行的方法,必須為公有void類型方法,不能有參數。
{?? int k,n;
for(k=0;k<4;k++)
{?? n=num;//取出num,可以把把num想象為總人數
n++;//加1
Thread.Sleep(10);//模擬復雜的費時運算,在此期間,有可能時間片用完
num=n;//存回num
Thread.Sleep(50);
}
label1.Text=Convert.ToString(num);
}//退出該方法,線程結束
public void Fun2()
{?? int k,n;
for(k=0;k<4;k++)
{?? n=num;
n++;
Thread.Sleep(10);
num=n;
Thread.Sleep(100);
}
label1.Text=Convert.ToString(num);
}
(6)?? 編譯,運行,標簽控件應顯示8,實際運行多次,顯示的數要小于8。
7.4.2? 用Lock語句實現互斥
Lock語句的形式如下:lock(e){訪問共享資源的代碼}。其中e指定要鎖定的對象,必須是引用類型,一般為this,即Lock語句所在類的對象。Lock語句將訪問共享資源的代碼標記為臨界區。臨界區的意義是:假設線程1正在執行e對象的臨界區中的代碼時,如其它線程也要求執行這個e對象的任何臨界區中代碼,將被阻塞,一直到線程1退出臨界區。
例子e7_4_2:用C#語句Lock實現互斥。修改例子e7_4_1中的Fun1()和Fun2()方法如下:
public void Fun1()//在線程中執行的方法,必須為公有void類型方法,不能有參數。
{?? int k,n;
for(k=0;k<4;k++)
{?? lock(this)//這里的this是Form1類的對象
{?? n=num;//這對大括號中代碼為this的臨界區
n++;//this的臨界區包含兩部分,函數Fun1和Fun2中的臨界區
Thread.Sleep(10);
num=n;
}
Thread.Sleep(50);
}
label1.Text=Convert.ToString(num);
}//退出該方法,線程結束
public void Fun2()
{?? int k,n;
for(k=0;k<4;k++)
{?? lock(this)//如有線程進入此臨界區,其它線程就不能進入這個臨界區
{?? n=num;//也不能進入前邊的臨界區
n++;
Thread.Sleep(10);
num=n;
}
Thread.Sleep(100);
}
label1.Text=Convert.ToString(num);
}
編譯,運行,標簽控件顯示8。如果有多個共享數據區,使用此方法不太方便。
7.4.3? 用Mutex類實現互斥
可以使用Mutex類對象保護共享資源(如上例中的總人數變量)不被多個線程同時訪問。Mutex類WaitOne方法和ReleaseMutex方法之間代碼是互斥體,這些代碼要訪問共享資源。Mutex類的WaitOne方法分配互斥體訪問權,該方法只向一個線程授予對互斥體的獨占訪問權。如果一個線程獲取了互斥體,則要獲取該互斥體的第二個線程將被掛起,直到第一個線程用ReleaseMutex方法釋放該互斥體。
例子e7_4_3:使用Mutex類對象實現互斥。修改例子e7_4_1,為Form1類增加私有Mutex類變量:private Mutex mut。在Form1類構造函數中建立Mutex類對象,在建立線程語句之前增加語句mut=new Mutex();修改例子e7_4_1中的兩個Fun1()和Fun2()方法如下:
public void Fun1()//在線程中執行的方法,必須為公有void類型方法,不能有參數。
{?? int k,n;
for(k=0;k<4;k++)
{?? mut.WaitOne();//等待互斥體訪問權
n=num;// mut.WaitOne()和mut.ReleaseMutex()之間是互斥體
n++;//Mutex類對象mut的互斥體包含兩部分,函數Fun1和Fun2中的互斥體
Thread.Sleep(10);//有線程進入一個互斥體,其它線程不能進入任何一個互斥體
num=n;
mut.ReleaseMutex();//釋放互斥體訪問權
Thread.Sleep(50);
}
label1.Text=Convert.ToString(num);
}//退出該方法,線程結束
public void Fun2()
{?? int k,n;
for(k=0;k<4;k++)
{?? mut.WaitOne();
n=num;
n++;
Thread.Sleep(10);
num=n;
mut.ReleaseMutex();
Thread.Sleep(100);
}
label1.Text=Convert.ToString(num);
}
編譯,運行,標簽控件顯示8。如果有多個共享數據區,可以定義多個Mutex類對象。
7.4.4? 用Monitor類實現互斥
也可以使用Monitor類保護共享資源不被多個線程或進程同時訪問。Monitor類通過向單個線程授予對象鎖來控制對對象的訪問。只有擁有對象鎖的線程才能執行臨界區的代碼,此時其它任何線程都不能獲取該對象鎖。只能使用Monitor類中的靜態方法,不能創建Monitor類的實例。Monitor類中的靜態方法主要有:
l 方法Enter:獲取參數指定對象的對象鎖。此方法放在臨界區的開頭。如其它線程已獲取對象鎖,則該線程將被阻塞,直到其它線程釋放對象鎖,才能獲取對象鎖。
l 方法Wait:釋放參數指定對象的對象鎖,以便允許其它被阻塞的線程獲取對象鎖。該線程進入等待狀態,等待狀態必須由其它線程用方法Pulse或PulseAll喚醒,使等待狀態線程變為就緒狀態。
l 方法Pulse和PulseAll:向等待線程隊列中第一個或所有等待參數指定對象的對象鎖的線程發送信息,占用對象鎖的線程準備釋放對象鎖。執行方法Exit后將釋放對象鎖。
l 方法Exit:釋放參數指定對象的對象鎖。此操作還標記受對象鎖保護的臨界區的結尾。
使用Monitor類實現互斥也很簡單,請讀者修改例子7_4_1,使用Monitor類實現互斥。Monitor類主要用來實現生產者和消費者關系中的線程的同步,具體例子見下一節。
7.5?? 生產者線程和消費者線程的同步
在生產者和消費者關系中,生產者線程產生數據,并把數據存到公共數據區,消費者線程使用數據,從公共數據區取出數據,并進行分析。很顯然,如果公共數據區只能存一個數據,那么在消費者線程取出數據前,生產者線程不能放新數據到公共數據區,否則消費者線程將丟失數據。同樣,只有在生產者線程把數據已經放到公共數據區,消費者線程才能取出數據,如果新數據未放到公共數據區,消費者線程不能取數據。這些就是所謂的生產者和消費者關系,必須要求生產者線程和消費者線程同步。
7.5.1? 生產者線程和消費者線程不同步可能發生錯誤
例子e7_5_1:下邊的例子模擬生產者線程和消費者線程不同步可能發生錯誤。有一個公共變量,要求生產者線程順序放1到4到這個公共變量中,每放一個變量,消費者線程取出這個數求和,最后把和顯示出來,顯然和應為10。如不采取同步措施,和的結果不正確。
(1)?? 新建項目。在Form1.cs頭部增加語句:using System.Threading。
(2)?? 在窗體中放置一個標簽控件,屬性Name=label1。
(3)?? 為Form1類定義2個線程類變量:Thread thread1,thread2。
(4)?? 為Form1類定義2個整形變量:int sum=0,x=-1。
(5)?? 為Form1類構造函數增加語句如下:
thread1= new Thread(new ThreadStart(Fun1));
thread2= new Thread(new ThreadStart(Fun2));
thread1.Start();
thread2.Start();
(6)?? 為Form1類定義Fun1()和Fun2()方法如下:
public void Fun1()//生產數據
{?? int k,n;
for(k=1;k<5;k++)
{?? x=k;
Thread.Sleep(200);
}
}
public void Fun2()//消費數據
{?? int k,n;
for(k=0;k<4;k++)
{?? sum+=x;
Thread.Sleep(100);
}
label1.Text=Convert.ToString(sum);
}
(7)?? 編譯,運行,標簽控件應顯示10,實際運行多次,顯示的數不為10。
7.5.2? 生產者線程和消費者線程同步的實現
修改上例,為Form1類定義1個布爾變量:bool mark=false。其值為false,表示數據還未放到公共數據區(即x)中,生產者線程可以放數據到公共數據區中,由于沒有數據,消費線程不能取數據,必須等待。mark=true,表示數據已放到公共數據區(即x)中,消費線程還未取數據,生產者線程不能再放數據到公共數據區中,必須等待。由于有了數據,消費線程可以取數據。修改Fun1()如下:
public void Fun1()//生產數據
{?? int k,n;
for(k=1;k<5;k++)
{?? Monitor.Enter(this);//這里this是Form1類對象,得到this的對象鎖
if(mark)//Monitor.Enter(this)和Monitor.Exit(this)是臨界區
Monitor.Wait(this);//如消費者數據未取走,釋放對象鎖,生產者等待
Mark=!mark;
x=k;
Monitor.Pulse(this);//激活消費者線程
Monitor.Exit(this);//釋放this的對象鎖
}
}
修改Fun2()如下:
public void Fun2()//消費數據
{?? int k,n;
for(k=0;k<4;k++)
{?? Monitor.Enter(this);
if(!mark)
Monitor.Wait(this);//如果生產者未放數據,消費者等待
Mark=!mark;
sum+=x;
Monitor.Pulse(this);
Monitor.Exit(this);
}
label1.Text=Convert.ToString(sum);
}
編譯,運行,標簽控件應顯示10。
轉載于:https://www.cnblogs.com/Aha-Best/p/10931701.html
總結
- 上一篇: 2019 课程设计个人报告
- 下一篇: 高中生怎么样才可以申请去美国读大学?