C#WinForm多線程開發(fā)_第1頁
C#WinForm多線程開發(fā)_第2頁
C#WinForm多線程開發(fā)_第3頁
已閱讀5頁,還剩4頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)

文檔簡介

1、C# WinForm 多線程開發(fā)一 Thread 類庫Windows 是一個多任務(wù)的系統(tǒng),如果你使用的是 windows 2000 及其以上版本,你可以通過 任務(wù)管理器查看當(dāng)前系統(tǒng)運行的程序和進(jìn)程。 什么是進(jìn)程呢?當(dāng)一個程序開始運行時, 它就 是一個進(jìn)程, 進(jìn)程所指包括運行中的程序和程序所使用到的內(nèi)存和系統(tǒng)資源。 而一個進(jìn)程又 是由多個線程所組成的,線程是程序中的一個執(zhí)行流,每個線程都有自己的專有寄存器(棧指針、程序計數(shù)器等 ),但代碼區(qū)是共享的,即不同的線程可以執(zhí)行同樣的函數(shù)。多線程是 指程序中包含多個執(zhí)行流,即在一個程序中可以同時運行多個不同的線程來執(zhí)行不同的任 務(wù),也就是說允許單個程序創(chuàng)

2、建多個并行執(zhí)行的線程來完成各自的任務(wù)。一 關(guān)于 Thread 的說明在 .net framework class library 中,所有與多線程機(jī)制應(yīng)用相關(guān)的類都是放在 System.Threading 命名空間中的。其中提供 Thread 類用于創(chuàng)建線程, ThreadPool 類用于管理線程池等等,此 外還提供解決了線程執(zhí)行安排, 死鎖, 線程間通訊等實際問題的機(jī)制。 如果你想在你的應(yīng)用 程序中使用多線程,就必須包含這個類。 Thread 類有幾個至關(guān)重要的方法,描述如下:Start():啟動線程Sleep(int) :靜態(tài)方法,暫停當(dāng)前線程指定的毫秒數(shù)Abort() :通常使用該方法來

3、終止一個線程Suspend():該方法并不終止未完成的線程,它僅僅掛起線程,以后還可恢復(fù)。Resume():恢復(fù)被Suspend()方法掛起的線程的執(zhí)行線程入口使程序知道該讓這個線程干什么事,在C#中,線程入口是通過ThreadStart代理(delegate)來提供的,你可以把ThreadStart理解為一個函數(shù)指針,指向線程要執(zhí)行的函數(shù),當(dāng)調(diào)用 Thread.Start()方法后,線程就開始執(zhí)行ThreadStart所代表或者說指向的函數(shù)。ThreadState在各種情況下的可能取值如下:Aborted :線程已停止AbortRequested :線程的Thread.Abort()方法已被

4、調(diào)用,但是線程還未停止Background :線程在后臺執(zhí)行,與屬性 Thread.IsBackground 有關(guān)Running :線程正在正常運行Stopped :線程已經(jīng)被停止StopRequested:線程正在被要求停止Suspended:線程已經(jīng)被掛起(此狀態(tài)下,可以通過調(diào)用Resume()方法重新運行)SuspendRequested:線程正在要求被掛起,但是未來得及響應(yīng)Un started :未調(diào)用Thread.Start()開始線程的運行WaitSleepJoin :線程因為調(diào)用了 Wait(),Sleep()或Join()等方法處于封鎖狀態(tài)二 Winform 中使用的 thre

5、ad首先可以看看最直接的方法,也是 .net 1.0 下支持的方法。但請注意的是,此方法在 .net 2.0 以后就已經(jīng)是一種錯誤的方法了。csharp view plain copy 在 CODE 上查看代碼片派生到我的代碼片 public partial class Form1 : Formpublic Form1()InitializeComponent();private void Form1_Load(object sender, EventArgs e)Thread thread = new Thread(ThreadFuntion);thread.IsBackground = t

6、rue;thread.Start();private void ThreadFuntion()while (true)this.textBox1.Text = DateTime.Now.ToString();Thread.Sleep(1000);這段 code 在 vs2005 或者 2008 上都拋出異常 : Cross-thread operation not valid:Control 'textBox1' accessed from a thread other than the thread it was created on . 這是因為 .net 2.0 以后 加

7、強(qiáng)了安全機(jī)制, 不允許在 winform 中直接跨線程訪問控件的屬性。 那么怎么解決這個問題 呢,下面提供幾種方案。第一種方案: 在 Thread 創(chuàng)建之氣,將 Control.CheckForIllegalCrossThreadCalls 設(shè)為 false。 此代碼告訴編譯器: 在這個類中我們不檢查跨線程的調(diào)用是否合法 (如果沒有加這句話運行 也沒有異常,那么說明系統(tǒng)以及默認(rèn)的采用了不檢查的方式) 。然而,這種方法不可取。我 們查看 CheckForIllegalCrossThreadCalls 這個屬性的定義, 就會發(fā)現(xiàn)它是一個 static 的,也就 是說無論我們在項目的什么地方修改了這

8、個值, 他就會在全局起作用。 而且像這種跨線程訪 問是否存在異常, 我們通常都會去檢查。 如果項目中其他人修改了這個屬性, 那么我們的方 案就失敗了,我們要采取另外的方案。第二種方案csharp view plain copy 在 CODE 上查看代碼片派生到我的代碼片 namespace TestInvokerpublic partial class Form1 : Formpublic Form1()InitializeComponent();private void button1_Click(object sender, EventArgs e)Thread thread = new

9、Thread(new ThreadStart(StartSomeWorkFromUIThread); thread.IsBackground = true;thread.Start();/StartSomeWorkFromUIThread();/label1.Text = "Set value through another thread!"private void StartSomeWorkFromUIThread()if (this.InvokeRequired)BeginInvoke(new EventHandler(RunsOnWorkerThread), null

10、);elseRunsOnWorkerThread(this, null);private void RunsOnWorkerThread(object sender, EventArgs e)Thread.Sleep(2000);label1.Text = System.DateTime.Now.ToString();通過上敘代碼, 可以看到問題已經(jīng)被解決了, 通過等待異步, 我們就不會總是持有主線程的 控制,這樣就可以在不發(fā)生跨線程調(diào)用異常的情況下完成多線程對 winform 多線程控件的控 制了。二 ThreadPool 與 Timer本文接上文,繼續(xù)探討 WinForm 中的多線程問題,

11、再次主要探討 threadpool 和 timer 。一 、 ThreadPool線程池(ThreadPool)是一種相對較簡單的方法,它適應(yīng)于一些需要多個線程而又較短任務(wù)(如一些常處于阻塞狀態(tài)的線程) ,它的缺點是對創(chuàng)建的線程不能加以控制,也不能設(shè)置其 優(yōu)先級。由于每個進(jìn)程只有一個線程池,當(dāng)然每個應(yīng)用程序域也只有一個線程池(對線),所 以 你 將 發(fā) 現(xiàn) ThreadPool 類 的 成 員 函 數(shù) 都 為 static ! 當(dāng) 你 首 次 調(diào) 用 ThreadPool.QueueUserWorkItem 、 ThreadPool.RegisterWaitForSingleObject 等,

12、便會創(chuàng)建線程 池實例。下面我就線程池當(dāng)中的兩函數(shù)作一介紹:csharp view plain copy 在 CODE 上查看代碼片派生到我的代碼片 public static bool QueueUserWorkItem( / 調(diào)用成功則返回 trueWaitCallback callBack,/ 要創(chuàng)建的線程調(diào)用的委托object state /傳遞給委托的參數(shù))/ 它的另一個重載函數(shù)類似 ,只是委托不帶參數(shù)而已 此函數(shù)的作用是把要創(chuàng)建的線程排隊到線程池, 當(dāng)線程池的可用線程數(shù)不為零時 (線程 池有創(chuàng)建線程數(shù)的限制,缺身值為25),便創(chuàng)建此線程,否則就排隊到線程池等到它有可用的線程時才創(chuàng)建。

13、csharp view plain copy 在 CODE 上查看代碼片派生到我的代碼片 public static RegisteredWaitHandle RegisterWaitForSingleObject(WaitHandle waitObject,/ 要注冊的 WaitHandleWaitOrTimerCallback callBack,/ 線程調(diào)用的委托object state,/ 傳遞給委托的參數(shù)int TimeOut,/ 超時,單位為毫秒,bool executeOnlyOnce / 是否只執(zhí)行一次);public delegate void WaitOrTimerCallb

14、ack(object state,/ 也即傳遞給委托的參數(shù)bool timedOut/true 表示由于超時調(diào)用,反之則因為 waitObject);此函數(shù)的作用是創(chuàng)建一個等待線程, 一旦調(diào)用此函數(shù)便創(chuàng)建此線程, 在參數(shù) waitObject 變?yōu)?終止?fàn)顟B(tài)或所設(shè)定的時間 TimeOut 到了之前,它都處于 “阻塞”狀態(tài),值得注意的一點是 此“阻塞”與 Thread 的 WaitSleepJoin 狀態(tài)有很大的不同:當(dāng)某 Thread 處于 WaitSleepJoin 狀態(tài)時 CPU 會定期的喚醒它以輪詢更新狀態(tài)信息,然后再次進(jìn)入WaitSleepJoin 狀態(tài),線程的切換可是很費資源的;而用

15、此函數(shù)創(chuàng)建的線程則不同,在觸發(fā)它運行之前, CPU 不會切 換到此線程, 它既不占用 CPU 的時間又不浪費線程切換時間, 但 CPU 又如何知道何時運行 它?實際上線程池會生成一些輔助線程用來監(jiān)視這些觸發(fā)條件, 一旦達(dá)到條件便啟動相應(yīng)的 線程, 當(dāng)然這些輔助線程本身也占用時間, 但是如果你需創(chuàng)建較多的等待線程時, 使用線程 池的優(yōu)勢就越加明顯。更詳細(xì)內(nèi)容 demo:csharp view plain copy 在 CODE 上查看代碼片派生到我的代碼片namespace TestMethodInvokerpublic partial class Form2 : Formpublic Form

16、2()InitializeComponent();private void button1_Click(object sender, EventArgs e)/ThreadPool.RegisterWaitForSingleObject(/ ev,/ new WaitOrTimerCallback(WaitThreadFunc),/ 4,/ 2000,/false/ 表示每次完成等待操作后都重置計時器,直到注銷等待/ );ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadFunc), "test1"); /Thread.

17、Sleep(10000);private delegate void MyInvokeDelegate(string name); private void Test(object o)richTextBox1.Text += string.Format("the object is 0 n", o);public void ThreadFunc(object b)this.Invoke(new MyInvokeDelegate(Test), b);public void WaitThreadFunc(object b, bool t)richTextBox1.Text +

18、= string.Format("the object is 0,t is 1n", b, t); 一個很值得擴(kuò)展的地方時,這里的 invoke 用的是代理,其實還有其他的方法,比如 action 和 func 。實例代碼如下:csharp view plain copy 在 CODE 上查看代碼片派生到我的代碼片 this.Invoke(new Action<string>(this.ChangeText), o.ToString();this.Invoke(new Action(delegate() this.textBox1.Text = o.ToStri

19、ng(););private void DoSomething(object o)System.Func<string, int> f = new Func<string, int>(this.GetId); object result = this.Ioke(f, o.ToString(); MessageBox.Show(result.ToString();private int GetId(string name)this.textBox1.Text = name;if (name = "Y")return 999;elsereturn 0;二

20、、 Timer它適用于需周期性調(diào)用的方法, 它不在創(chuàng)建計時器的線程中運行, 它在由系統(tǒng)自動分配的單 獨線程中運行。這和 Win32 中的 SetTimer 方法類似。它的構(gòu)造為:csharp view plain copy 在 CODE 上查看代碼片派生到我的代碼片public Timer(TimerCallback callback,/ 所需調(diào)用的方法object state,/ 傳遞給 callback 的參數(shù)int dueTime,/ 多久后開始調(diào)用 callbackint period/ 調(diào)用此方法的時間間隔);/如果 dueTime 為 0,則 callback 立即執(zhí)行它的首次調(diào)用

21、。如果 dueTime 為 Infinite ,則 callback 不調(diào)用它的方法。 計時器被禁用, 但使用 Change 方法可以重新啟用它。 如果 period 為 0 或 Infinite ,并且 dueTime 不為 Infinite ,則 callback 調(diào)用它的方法一次。計時器的定 期行為被禁用,但使用 Change 方法可以重新啟用它。如果 period 為零 (0) 或 Infinite , 并且 dueTime 不為 Infinite ,則 callback 調(diào)用它的方法一次。計時器的定期行為被禁用, 但使用 Change 方法可以重新啟用它。在創(chuàng)建計時器之后若想改變它的

22、period和dueTime,我們可以通過調(diào)用Timer的Change方法來改變:csharp view plain copy 在 CODE 上查看代碼片派生到我的代碼片public bool Change(int dueTime,int period);/顯然所改變的兩個參數(shù)對應(yīng)于 Timer 中的兩參數(shù)。三 Control.Invoke下面我們就把在 Windows Form 軟件中使用 Invoke 時的多線程要注意的問題給大家做 一個介紹。首先,什么樣的操作需要考慮使用多線程?總的一條就是,負(fù)責(zé)與用戶交互的線程(以下簡稱為UI線程)應(yīng)該保持順暢,當(dāng)UI線程調(diào)用的API可能引起阻塞時間超

23、過 30毫秒時(比 如訪問 CD-ROM 等速度超慢的外設(shè)、 進(jìn)行遠(yuǎn)程調(diào)用等等 )就應(yīng)該考慮使用多線程。 為什么是 30 毫秒? 30 毫秒的概念是人眼可以察覺到的一個遲滯,大約等同于電影里的一幀停留的時 間,最長不要超過 100 毫秒。第二,最方便和簡單的多線程是使用線程池。通過線程池里的線程運行代碼的最簡便 方法則是使用異步委托調(diào)用。注意委托調(diào)用通常是同步完成的,請使用 BeginInvoke 方法, 這樣就可以把要調(diào)用的方法排隊到線程池里等候處理,而程序的流程會立刻返回到調(diào)用方 (此處是 UI 線程 ),而調(diào)用方因此不會出現(xiàn)阻塞??纯聪旅娴睦游覀兙桶l(fā)現(xiàn)要使用線程池異步執(zhí)行代碼也并非十分

24、復(fù)雜,這里我們利 用 System.Windows.Forms.MethodInvoker 委托進(jìn)行異步調(diào)用。注意 MethodInvoker 委托不接 受方法參數(shù),如果需要向異步執(zhí)行的方法傳遞參數(shù),請使用其他委托,或者需要自己定義。csharp view plain copy 在 CODE 上查看代碼片派生到我的代碼片private void StartSomeWorkFromUIThread () / 我們要做的工作相對 UI 線程而言臺慢了,用下面的方法異步進(jìn)行處理 MethodInvoker mi = new MethodInvoker(RunsOnWorkerThread);/ 這是

25、入口方法 mi.BeginInvoke(null, null); / 這樣就不會阻塞/ 緩慢的工作在此方法內(nèi)進(jìn)行處理,使用線程池里的線程private void RunsOnWorkerThread() DoSomethingSlow();歸納上述方法,對 UI 線程而言實際上就是: 1、發(fā)出調(diào)用, 2、立刻返回,具體運行過 程不理了,這樣 UI 線程就不會被阻塞。這種方法很重要,下面我們會深入介紹。除了上面 的方法,還有其他使用線程池的方法,當(dāng)然如果你高興也可以自己創(chuàng)建線程。第三,在 Windows Form 中使用多線程的,最重要的一條注意事項是,除了創(chuàng)建控件的 線程以外,絕對不要在任何其

26、他線程里面調(diào)用控件的成員(只有極個別情況例外 ),也就是說控件屬于創(chuàng)建它的線程,不能從其他線程里面訪問。這一條適用于所有從 System.Windows.Forms.Control 派生的控件 ( 因此可以說是幾乎所有控件),包括 Form 控件本身也是。 舉一反三, 我們很容易得出這樣的結(jié)論, 控件的子控件必須由創(chuàng)建控件的線程來創(chuàng) 建,比如一個表單上的按鈕,比如由創(chuàng)建表單的線程來創(chuàng)建, 因此,一個窗口中的所有控件 實際上都活在同一個線程之中。 在實際編程時, 大多數(shù)的軟件的做法都是讓同一線程負(fù)責(zé)全 部的控件,這就是我們所說的 UI 線程??聪旅娴睦樱篶sharp view plain co

27、py 在 CODE 上查看代碼片派生到我的代碼片/ 這是由 UI 線程定義的 Label 控件private Label lblStatus;/ 以下方法不在 UI 線程上執(zhí)行private void RunsOnWorkerThread() DoSomethingSlow();lblStatus.Text = "Finished!" / 這是錯的 我們要特別提醒大家,很多人剛開始的時候都會使用以上的方法來訪問不在同一個線程里的控件(包括筆者本人),而且在1.0版.Net框架上似乎沒有發(fā)現(xiàn)問題,但是這根本就是錯的, 更糟糕的是, 程序員在這里不會得到任何錯誤提示,一開始就上

28、當(dāng)受騙,之后會莫明 其妙地發(fā)現(xiàn)其他錯誤,這就是 Windows Form 多線程編程的痛苦所在。筆者試過花很多時間 來 Debug 自己寫的 Splash 窗口突然消失的問題,結(jié)果還是失敗了:筆者在軟件的引導(dǎo)過程 中,用另外一個線程里創(chuàng)建了一個 Splash 窗口來顯示歡迎信息,然后嘗試把主線程里引導(dǎo) 的狀態(tài)直接寫入到 Splash窗口上的控件中,開始還0K,可是過一會Splash窗口就莫明其妙 消失了。理解了這一點, 我們應(yīng)該留意到, 有時候即使沒有用 System.Threading.Thread 來顯式創(chuàng) 建一個線程, 我們也可能因為使用了異步委托的 BeginInvoke 方法來隱式創(chuàng)

29、建了線程 (從線程 池里),在這種線程里也同樣不能調(diào)用 UI 線程所創(chuàng)建的控件的成員。第四,由于上述限制,我們可能會感到很不方便,的確,當(dāng)我們利用一個新創(chuàng)建的線 程來執(zhí)行某些花時間的運算時,怎樣知道運算進(jìn)度如何并通過UI 反映給用戶呢?解決方法很多! 比如熟悉多線程編程的用戶很快會想到,我們采用一些低級的同步方法, 工作者線程把狀態(tài)保存到一個同步對象中,讓 UI 線程輪詢 (Polling) 該對象并反饋給用戶就可以了。不 過,這還是挺麻煩的,實際上不用這樣做, Control 類( 及其派生類 ) 對象有一個 Invoke 方法 很特別, 這是少數(shù)幾個不受線程限制的成員之一。 我們前面說到,

30、 絕對不要在任何其他線程 里面調(diào)用非本線程創(chuàng)建的控件的成員時,也說了“只有極個別情況例外” ,這個 Invoke 方法 就是極個別情況之一 Invoke 方法可以從任何線程里面調(diào)用。 下面我們來講解 Invoke 方法。Invoke 方法的參數(shù)很簡單, 一個委托, 一個參數(shù)表 (可選 ) ,而 Invoke 方法的主要功能就 是幫助你在 UI 線程 (即創(chuàng)建控件的線程 )上調(diào)用委托所指定的方法。 Invoke 方法首先檢查發(fā) 出調(diào)用的線程 (即當(dāng)前線程 )是不是 UI 線程,如果是,直接執(zhí)行委托指向的方法,如果不是, 它將切換到 UI 線程,然后執(zhí)行委托指向的方法。不管當(dāng)前線程是不是 UI 線程, Invoke 都 阻塞直到委托指向的方法執(zhí)行完畢,然后切換回發(fā)出調(diào)用的線程(如果需要的話 ),返回。注意,使用 Invoke 方法時, UI 線程不能處于阻塞狀態(tài)。以下 MSDN 里關(guān)于 Invoke 方法的說 明:plain view plain copy 在 C0DE 上查看代碼片派生到我的代碼片“控件上有四種方法可以安全地從任何線程進(jìn)行調(diào)用:Invoke 、BeginInvoke 、EndInvoke和CreateGraphics。對于所有其他方法調(diào)用,則應(yīng)使用調(diào)用(invoke)方法之一封送對控件的線程的調(diào)用。委托可以是 EventHandler 的

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論