適用于:
Microsoft .NET Compact Framework version 2.0
用于 Pocket PC 的 Windows Mobile 5.0 軟件
Microsoft Visual C#
Microsoft Visual Studio 2005
摘要:在此自己控制進度的動手體驗中,學習如何將 Microsoft .NET Compact Framework 2.0 的強大支持用于托管多線程應用程序,同時避免多線程帶來的諸多復雜性。您將學習如何正確創建和終止線程、如何處理從內部工作線程更新用戶界面控件所帶來的問題以及在不同的時期使用哪些同步對象。完成此體驗之后,您將學會如何使用 .NET Compact Framework 2.0 的多線程功能來創建響應速度快的應用程序,這些應用程序面向基于 Windows CE 5.0 和基于 Windows Mobile 的設備。此體驗的技術等級為 Level 300(中級),您在 45 分鐘內應該能夠完成。
請從 Microsoft 下載中心下載 MED204_Dev_Multithread_Apps_NETCF2.msi。
本頁內容
![]() |
簡介 |
![]() |
練習 1:使用 .NET Compact Framework 2.0 創建多線程應用程序 |
![]() |
練習 2:修改多線程應用程序 |
![]() |
練習 3:檢查 Thread 和 ThreadPool |
![]() |
練習 4:更新線程內的用戶界面控件 |
![]() |
練習 5:使用同步對象來同步線程 |
![]() |
總結 |
要完成此練習,您需要具備:
• |
Windows XP Professional。 | ||||
• |
Visual Studio 2005。 | ||||
• |
ActiveSync 4.0。 | ||||
• |
Windows Mobile 5.0 SDK。
|
簡介
此體驗的目標是說明 Microsoft .NET Compact Framework 2.0 版的多線程功能,使用該多線程功能可創建響應的應用程序,這些應用程序面向基于 Microsoft Windows CE 5.0 版的設備和/或基于 Windows Mobile 的設備。完成體驗中的所有練習所需的時間可能比您想投入的時間長,因此可以僅選擇對您最有用的那些練習來做。每個練習都以一個全新的項目開始,因此進行這些練習并沒有明確的順序必須要遵守。但是,如果您對托管線程開發還很陌生,則應從第一個練習開始做起。
• |
使用 .NET Compact Framework 2.0 創建多線程應用程序 |
• |
修改多線程應用程序 |
• |
檢查 Thread 和 ThreadPool |
• |
更新線程內的用戶界面控件 |
• |
使用同步對象來同步線程 |
練習 1:使用 .NET Compact Framework 2.0 創建多線程應用程序
在此練習中,您將生成一個單線程 Pocket PC 應用程序,該應用程序在練習 2 中將被轉換為多線程應用程序。
注意:您可能還會看到一個對話框,指明 VPC 網絡驅動程序無法打開?梢院雎源司,單擊 OK(確定)。
創建新的空 Pocket PC 應用程序的步驟
1. |
通過雙擊桌面上的圖標啟動 Microsoft Visual Studio 2005。 |
2. |
在 File(文件)菜單上,單擊 NewProject(新建項目)。將出現 NewProject(新建項目)對話框。 |
3. |
在 Project(項目)類型下,確保選擇了 Visual C# Projects - Smart Device -PocketPC2003(Visual C# 項目 - 智能設備 - Pocket PC 2003)。 |
4. |
在 Templates(模板)下,確保選擇了 DeviceApplication(設備應用程序)。 |
5. |
在 Name(名稱)框中,鍵入 MultithreadedApp 作為該應用程序的名稱。 |
6. |
在 SolutionName(解決方案名稱)框中,鍵入 MultithreadedApp 作為解決方案的名稱。 |
7. |
在 Location(位置)框中鍵入 C:\labs\MultithreadedLab\Lab(或者使用您的首選的驅動器)。 |
8. |
確保選擇了 Createdirectory for solution(創建解決方案的目錄)復選框,如下圖所示。 ![]() |
9. |
單擊 OK(確定)創建一個空項目,如下圖所示。 ![]() |
雖然您剛創建的應用程序沒有多大用處,但它是一個完整的 Pocket PC 應用程序,因此可以對其進行編譯,并將其部署到目標設備。對于此體驗中的所有練習,您都將使用 Pocket PC 仿真器來完成。在下一個任務中,您將會將此第一個應用程序部署到仿真器,以便可以測試與仿真器的連接。
生成應用程序并將其部署到 Pocket PC 仿真器的步驟
1. |
在 Visual Studio 菜單上,驗證是否選擇了 Pocket PC 2003 SE Emulator(Pocket PC 2003 SE 仿真器)作為目標設備。 |
2. |
在目標設備框右側的工具欄上,單擊 Connect to device(連接到設備)按鈕,以建立與 Pocket PC 2003 SE 仿真器的連接。 您將看到 Pocket PC 仿真器開始啟動。與 Visual Studio 2005 建立完連接之后,Connecting(連接)對話框中將出現確認消息,如下圖所示。 ![]() 現在您就可以在 Pocket PC 仿真器上部署和運行您的應用程序了。 |
3. |
通過單擊 Close(關閉)來關閉 Connecting(連接)對話框。 |
4. |
通過按 F5 或單擊 Debug(調試)菜單上的 Start Debugging(啟動調試)在調試模式下啟動該應用程序。 |
5. |
在 Deploy MultithreadedApp(部署 MultithreadedApp)對話框中,確保選擇了 Pocket PC 2003SE Emulator(Pocket PC 2003 SE 仿真器)設備,然后再單擊 Deploy(部署),如下圖所示。 ![]() Visual Studio 2005 集成開發環境 (IDE) 中的狀態欄會通知您有關部署的進度。 |
6. |
確保通過單擊 Windows 任務欄上的仿真器按鈕,能夠看見仿真器。 稍后,您將看到在 Pocket PC 2003 SE 仿真器內部運行的該空應用程序,如下圖所示。此應用程序現在還沒有多大用處,因此您要向其添加某種功能。 ![]() |
向該應用程序添加功能的步驟
1. |
通過按 SHIFT+F5 或單擊 Debug(調試)菜單上的 StopDebugging(停止調試),從 Visual Studio 2005 內部退出該應用程序。 注意:如果您退出仿真器,則需要保存仿真器狀態。 |
2. |
單擊窗體中的某處并更改其 Text 屬性,將該應用程序的標題更改為 MultithreadedApp,如下圖所示。 ![]() 注意:如果未顯示 Properties(屬性)窗格,則可以通過單擊 View(查看)菜單上的 Properties(屬性)窗口來顯示。 |
3. |
為了更易于退出該應用程序,將窗體的 MinimizeBox 屬性更改為 False。這樣,就可以退出該應用程序,而無需通過將其智能最小化來退出。 現在,可以向應用程序添加功能了。對于第一個練習,您將向窗體添加兩個按鈕和一個模擬后臺處理的方法。 |
4. |
從設備控件列表的工具箱上選擇一個 Button,并將其拖到窗體上。 |
5. |
單擊該按鈕,然后通過使用該按鈕的 Text 屬性將其名稱更改為 Beginprocessing。 |
6. |
雙擊窗體中的 Beginprocessing 按鈕以添加單擊事件處理程序。 |
7. |
將以下代碼添加到 Button1_Click 事件處理程序: button1.Enabled = false; processingDone = false; BackgroundProcessing(); |
8. |
返回到 Visual Studio 中的設計視圖,然后從設備控件列表的工具箱上選擇另一個按鈕,并將其拖到窗體上。 |
9. |
單擊該按鈕,然后通過使用 Text 屬性將該按鈕名稱更改為 End processing。 |
10. |
雙擊窗體中的 End processing 按鈕以添加單擊事件處理程序。 |
11. |
將以下代碼添加到 button2_Click 處理程序: processingDone = true; button1.Enabled = true; |
12. |
通過在 Forml.cs 文件(本練習中所有代碼都將添加到此文件中)開始處的其他 using 語句下添加以下語句,為 System.Threading 命名空間創建一個別名: using System.Threading; |
13. |
將名為 processingDone 的布爾型實例變量添加到 Form1 類中。 private bool processingDone = false; |
14. |
使用以下代碼將名為 BackgroundProcessing 的新方法添加到 Form1 類中: private void BackgroundProcessing() { while (! processingDone) { Thread.Sleep(0); } } |
15. |
通過按 F5 或單擊 Debug(調試)菜單上的 Start Debugging(啟動調試)在調試模式下啟動該應用程序。 |
16. |
在 Deploy MultithreadedApp(部署 MultithreadedApp)對話框中,確保選擇了 Pocket PC 2003 SE Emulator(Pocket PC 2003 SE 仿真器)設備,然后再單擊 Deploy(部署)。 如果代碼編譯后未出現任何語法錯誤,則該應用程序將會在 Pocket PC 2003 SE 仿真器中出現。此過程可能會花費一點時間。 |
17. |
在仿真器中,單擊該應用程序的 Beginprocessing 按鈕,如下圖所示。 ![]() 您將看到 Beginprocessing 按鈕被禁用。沒有任何進一步的跡象表明 BackgroundProcessing 函數正在運行。 |
18. |
在仿真器中,單擊應用程序的 Endprocessing 按鈕。 您可能想要重新啟用 Beginprocessing 按鈕,但卻無法實現。實際上,該應用程序停止了響應。因為該應用程序的所有代碼都在同一線程的內部運行,所以出現了一個問題。執行完 BackgroundProcessing 方法中的代碼之后,無法調用 button2_Click 事件處理程序。這意味著該應用程序不會響應按鈕單擊,導致 processingDone 變量仍保持 false。換言之,該應用程序將無法正確關閉,如下圖所示。 ![]() |
若要解決此問題,您需要更改該應用程序,使其在一個單獨的線程中運行 BackgroundProcessing 方法。
練習 2:修改多線程應用程序
在此練習中,您將修改在前一個練習中創建的應用程序。這次,您將創建一個將在其中執行 BackgroundProcessing 方法的新線程。
如果您尚未通過按 SHIFT+F5 或單擊 Debug(調試)菜單上的 StopDebugging(停止調試)從 Visual Studio 2005 內部退出該應用程序,則執行此操作。如果 Visual Studio 2005 的 Debug(調試)工具欄上的 Stop(停止)按鈕可見,您也可以使用該按鈕從 Visual Studio 2005 內部退出該應用程序。
若要使該應用程序更有用,您現在需要創建一個將要運行 BackgroundProcessing 方法的新線程。在 .NET Compact Framework 中創建一個線程只需要實例化 Thread 類型的一個類并向該類傳遞一個對函數的引用。該函數是新線程的入口點,函數退出后,線程將自動終止。
創建將在其中執行 BackgroundProcessing 的單獨線程的步驟
1. |
將一個名為 workerThread 的 Thread 類型的實例變量添加到 processingDone 變量下的 Form1 類中。 private Thread workerThread; |
2. |
找到對 button1_Click 事件處理程序內部的 BackgroundProcessing() 的方法調用,并將其與以下語句進行替換: workerThread = new Thread(new ThreadStart(BackgroundProcessing)); workerThread.Start(); 這就是使 BackgroundProcessing 方法在其他線程中運行所需執行的全部操作。創建后調用線程的 Start 方法是非常重要的,否則該線程將不會運行,F在是再次運行該應用程序的時候了,注意其與先前運行的首版之間的不同。 |
3. |
通過按 F5 或單擊 Debug(調試)菜單上的 StartDebugging(啟動調試)在調試模式下啟動該應用程序。 |
4. |
在 Deploy MultithreadedApp(部署 MultithreadedApp)對話框中,確保選擇了 Pocket PC 2003SE Emulator(Pocket PC 2003 SE 仿真器)設備,然后再單擊 Deploy(部署)。 如果代碼編譯后未出現語法錯誤,則該應用程序將會在 Pocket PC 2003 SE 仿真器中出現,通過單擊仿真器的任務欄按鈕會使其可見。 |
5. |
在仿真器中,單擊應用程序的 Beginprocessing 按鈕。 您將看到 Beginprocessing 按鈕被禁用。沒有任何進一步的跡象表明 BackgroundProcessing 函數正在運行。 |
6. |
在仿真器中,單擊應用程序的 Endprocessing 按鈕。 現在您將看到 Beginprocessing 按鈕已再次啟用,這有效地說明了在其中執行 BackgroundProcessing 方法的線程已經終止。因此,此時您仍然控制著該應用程序。 |
在 .NET Compact Framework 2.0 中存在對前臺線程和后臺線程的區分,F在,您將體驗兩者行為中的不同之處。為此,您需要再次啟動應用程序中的工作線程。
正確終止創建的線程的步驟
1. |
在仿真器中,單擊應用程序的 Beginprocessing 按鈕。 |
2. |
在該應用程序的右上角,單擊 OK(確定)。 由于該應用程序的窗體已關閉,因此看上去該應用程序也將關閉。然而,如果您查看 Visual Studio,也許會注意到實際上該應用程序仍在運行,因為調試程序還處于活動狀態。其原因在于您所創建的線程是一個前臺線程。只要應用程序中還在運行前臺線程,該應用程序就不會關閉(即使其窗體已經關閉)。若要正確退出該應用程序,則您將需要添加某種額外功能。例如,可以添加檢查工作線程是否仍處于活動狀態的窗體關閉事件處理程序。在此情況下,第一次終止工作線程時用戶會收到一條消息。 |
3. |
通過按 SHIFT+F5 或單擊 Debug(調試)菜單上的 StopDebugging(停止調試),從 Visual Studio 2005 內部退出該應用程序。如果 Visual Studio 2005 的 Debug(調試)工具欄上的 Stop(停止)按鈕可見,您也可以使用該按鈕從 Visual Studio 2005 內部退出該應用程序。 |
4. |
在 Visual Studio 2005 中,單擊 Form1.cs [Design] 選項卡,然后單擊窗體上按鈕之外的某處。 |
5. |
在 Properties(屬性)窗口中,單擊工具欄上的 Events(事件)按鈕。 |
6. |
雙擊 Closing 事件,如下圖所示。 ![]() |
7. |
您現在已經創建了一個 Form1_Closing 事件處理程序,您需要向其添加以下代碼。 if (!processingDone) { MessageBox.Show("關閉應用程序前終止工作線程"); e.Cancel = true; } |
8. |
通過按 F5 或單擊 Debug(調試)菜單上的 StartDebugging(啟動調試)在調試模式下啟動該應用程序。 |
9. |
在 Deploy MultithreadedApp(部署 MultithreadedApp)對話框中,確保選擇了 Pocket PC 2003SE Emulator(Pocket PC 2003 SE 仿真器)設備,然后再單擊 Deploy(部署)。 |
10. |
在仿真器中,單擊應用程序的 Beginprocessing 按鈕。 |
11. |
在該應用程序的右上角,單擊 OK(確定)。 一個消息框會提示 BackgroundProcessing 線程仍在運行,并表明除非終止 BackgroundProcessing 線程,否則將不會關閉該應用程序的窗體。 |
12. |
單擊 OK(確定)關閉該消息框。 |
13. |
通過單擊 End Processing 按鈕,再單擊該應用程序右上角中的 OK(確定)退出該應用程序。 在 .NET Compact Framework 2.0 中,您可以通過將 Thread 類的 IsBackground 屬性設置為 true,將工作線程更改為后臺線程。屬于同一進程的最后一個前臺線程一結束,公共語言運行時就將自動終止線程。若要查看后臺線程的行為,您需要刪除剛創建的 Form1_Closing 事件處理程序。 |
14. |
在 Visual Studio 2005 中,單擊 Form1.cs [Design] 選項卡,然后單擊窗體上按鈕之外的某處。 |
15. |
在 Properties(屬性)窗口中,單擊工具欄上的 Events(事件)按鈕。 |
16. |
右鍵單擊 Closing 事件,再單擊 Reset(重置)。此操作會使事件處理程序從窗體中分離出來。 |
17. |
事件處理程序的代碼可能仍在您的源文件中。如果是這樣,則只從 Form1.cs 源文件中刪除整個 Form1_Closing 方法。 |
18. |
找到 Form1.cs 內部的 button1_Click 事件處理程序,并在創建和啟動該線程的語句之間添加以下語句: workerThread.IsBackground = true; |
19. |
通過按 F5 或單擊 Debug(調試)菜單上的 StartDebugging(啟動調試)在調試模式下啟動該應用程序。 |
20. |
在 Deploy MultithreadedApp(部署 MultithreadedApp)對話框中,確保選擇了 Pocket PC 2003 SE Emulator(Pocket PC 2003 SE 仿真器)設備,然后再單擊 Deploy(部署)。 |
21. |
在仿真器中,單擊應用程序的 Beginprocessing 按鈕。 |
22. |
在該應用程序的右上角,單擊 OK(確定)。 這次,即使 BackgroundProcessing 線程處于活動狀態,該應用程序仍將正確關閉。 |
在某些情況下,知道某線程是何時終止的對于應用程序而言是絕對必要的。對于這些情況,可以使用 .NET Compact Framework 2.0 中的 Thread.Join 方法。該方法會阻止調用線程,直到線程終止為止。若要使 Thread.Join 的行為更明了,您可以向工作線程添加一些耗時的終止代碼。
查看工作線程是否確實終止的步驟
1. |
找到 Form1.cs 源文件中的 BackgroundProcessing 方法。在 while 循環之下,添加以下語句: Thread.Sleep(2000); |
2. |
找到 button2_Click 事件處理程序。在語句: processingDone = true 之下,添加以下語句: workerThread.Join(); |
3. |
編譯、部署并運行該應用程序。 |
4. |
單擊 Beginprocessing 按鈕啟動工作線程,然后通過單擊 Endprocessing 按鈕再次終止該線程。 您可能會注意到,單擊 Endprocessing 按鈕之后,再次啟用 Beginprocessing 會花費幾秒鐘。其原因在于現在主線程在 button2_Click 事件處理程序中受到阻止,直到工作線程終止為止。因為已經向工作線程中添加了額外耗時 2 秒鐘的 Thread.Sleep,所以工作線程還需另外 2 秒鐘才能終止。 |
5. |
在該應用程序的右上角,單擊 OK(確定)退出該應用程序。 |
到目前為止,您已經結合使用了一個布爾型標志和一個 while 循環來停止工作線程。在 .NET Compact Framework 2.0 中,還有另一種終止工作線程的方法:可以將 Thread.Abort 方法與異常處理程序結合使用。如果必須始終執行某線程未初始化代碼(例如,關閉與某數據庫的連接),Thread.Abort 會非常有用。調用 Thread.Abort 會引發線程中被調用的 ThreadAbortException 開始終止該線程。
使用 Thread.Abort 終止線程的步驟
1. |
找到 Form1.cs 源文件中 button2_Click 事件處理程序的 processingDone = true 語句,將其替換成以下語句。 workerThread.Abort(); |
2. |
使用以下代碼替換 BackgroundProcessing 方法的所有代碼。 private void BackgroundProcessing() { try { while (!processingDone) { Thread.Sleep(0); } } catch (ThreadAbortException e) { MessageBox.Show(e.Message); } finally { // 該未初始化代碼必須始終執行, // 因此請確保將其放置在 finally 子句中。 Thread.Sleep(2000); } } |
3. |
編譯、部署并運行該應用程序。 |
4. |
單擊 Beginprocessing 按鈕啟動工作線程,然后通過單擊 Endprocessing 按鈕再次終止該線程。 您將看到 ThreadAbortException 已引發(如下圖所示),并且工作線程會在處理完異常之后終止。 ![]() |
5. |
單擊 OK(確定)關閉 ThreadAbortException 對話框,再單擊 OK(確定)退出該應用程序。 因為異常處理是托管代碼中代價較高的操作,所以在通常情況下將布爾型變量和 while 循環結合使用來正確終止工作線程也許會更好。 |
在多線程操作系統(如 Windows CE)中,各線程可以具有不同的優先級。將不同的優先級分配給不同的線程可以極大地影響應用程序的行為。高優先級的線程將始終先于低優先級的線程運行。當同一優先級的多個線程共存于 Windows CE 中時,它們將平均地共享處理器時間。若要查看更改線程優先級所帶來的影響,可賦予工作線程比主線程更高的優先級。然而,首先您需要修改線程方法本身,以使其繼續使用處理器。
注意:永遠不要在實際的應用程序中這樣做。
分配線程優先級的步驟
1. |
找到 Form1.cs 源文件中 BackgroundProcessing 方法的 Thread.Sleep(0) 語句,將其替換成一句空語句(只有一個分號)。 |
2. |
編譯、部署并運行該應用程序。 |
3. |
單擊 Beginprocessing 按鈕啟動工作線程,然后通過單擊 Endprocessing 按鈕再次終止該線程。 |
4. |
ThreadAbortException 對話框將出現。單擊 OK(確定),再單擊 OK(確定)退出該應用程序。 因為同一優先級的線程平均地共享處理器,所以現在即使工作線程想要繼續運行,您仍然可以將其終止。但是,現在來看一下如果更改了工作線程的優先級將會發生什么。 |
5. |
找到 Form1.cs 源文件中 button1_Click 事件處理程序的 workerThread.Start() 語句,在其上方添加以下語句: workerThread.Priority = ThreadPriority.AboveNormal; |
6. |
編譯、部署并運行該應用程序。 |
7. |
單擊 Beginprocessing 按鈕啟動工作線程,然后通過單擊 Endprocessing 按鈕再次終止該線程。 正如您所看到的,不可能再終止該工作線程了,更為嚴重的是無法退出該應用程序。其原因在于現在具有更高優先級的線程正在繼續運行,同時不允許執行主線程。因為主線程負責所有 UI 操作,所以用戶不能再控制該應用程序了。退出該應用程序的唯一方法是從 Visual Studio 2005 內部停止調試。 |
8. |
通過按 SHIFT+F5 或單擊 Debug(調試)菜單上的 StopDebugging(停止調試),從 Visual Studio 2005 內部退出該應用程序。如果 Visual Studio 2005 的 Debug(調試)工具欄上的 Stop(停止)按鈕可見,您也可以使用該按鈕從 Visual Studio 2005 內部退出該應用程序。 |
到目前為止,您已經創建了一個多線程應用程序,了解了前臺線程和后臺線程之間的區別,并學習了如何正確終止線程和多線程應用程序。您還了解了更改線程優先級可能會帶來的影響。在下一個練習中,您將比較 Thread 對象和 ThreadPool 對象。
練習 3:檢查 Thread 和 ThreadPool
對于此練習,您將創建一個新的應用程序,并再次使用 Pocket PC 2003 仿真器作為目標設備。
創建新的 Pocket PC 應用程序的步驟
1. |
在 Visual Studio 2005 中的 File(文件)菜單上,單擊 NewProject(新建項目)。 |
2. |
在 Project(項目)類型下,確保選擇了 Visual C# Projects - Smart Device - Pocket PC 2003(Visual C# 項目 - 智能設備 - Pocket PC 2003)。 |
3. |
在 Templates(模板)下,確保選擇了 DeviceApplication(設備應用程序)。 |
4. |
在 Name(名稱)框中鍵入 ThreadPoolvsThread。 |
5. |
在 Location(位置)框中鍵入 C:\labs\MultithreadedLab\Lab(或者使用您首選的驅動器)。 |
6. |
確保選擇了 Create directory for solution(創建解決方案的目錄)復選框。 |
7. |
單擊 OK(確定)創建空項目。 |
8. |
通過單擊窗體中的某處并更改其 Text 屬性,將該應用程序的標題更改為 ThreadPoolvsThread。如果未顯示 Properties(屬性)窗口,則可以通過單擊 View(查看)菜單上的 PropertiesWindow(屬性窗口)來顯示。 |
9. |
為了更易于退出該應用程序,將窗體的 MinimizeBox 屬性更改為 False。 |
10. |
將兩個 Button 控件和兩個 TextBox 控件添加到該應用程序的窗體上。 |
11. |
通過清除兩個 TextBox 控件的 Text 屬性使其全部為空,然后將其 ReadOnly 屬性設置為 true。 |
12. |
通過單擊兩個按鈕的 Text 屬性更改其名稱,如下圖所示。 ![]() |
13. |
在設計視圖中雙擊這兩個按鈕以添加單擊事件處理程序。添加完第一個單擊處理程序后必須切換回設計視圖,這樣才能添加第二個事件處理程序。 現在您已經結束了該應用程序用戶界面部分的創建。下一個任務是向該應用程序添加功能,以比較 Thread 類和 ThreadPool 類的行為。 |
14. |
通過在 Forml.cs 文件(本練習中所有代碼都將添加到此文件中)開始處的其他 using 語句下添加以下語句,為“System.Threading”命名空間創建一個別名: using System.Threading; |
15. |
將以下實例變量添加到 Form1 類中。 private AutoResetEvent doneEvent;private int threadCounter = 0;private int threadPoolCounter = 0; |
16. |
假定您已將在窗體上創建的第一個按鈕命名為 CreateThreads,請將以下代碼添加到 button1_Click 事件處理程序中。 button1.Enabled = false; threadCounter = 0; doneEvent = new AutoResetEvent(false); textBox1.Text = ""; int elapsedTime = Environment.TickCount; for (int i = 0; i < 200; i++) { Thread workerThread = new Thread(new ThreadStart(MyWorkerThread)); workerThread.Start(); } doneEvent.WaitOne(); elapsedTime = Environment.TickCount - elapsedTime; textBox1.Text = "正在創建線程:" + elapsedTime.ToString() + " msec"; button1.Enabled = true; 您剛加入到 button1_Click 事件處理程序的代碼會創建 200 個生存期都很短的不同的工作線程。實際上,所有工作線程的唯一功能是檢查某個具體線程是否為第 200 個線程。在此情況下,設置一個事件來通知主線程已完成測試的運行,這樣主線程就可以使用計時信息更新用戶界面了。所有工作線程都將共享您現在要添加的 MyWorkerThread 方法中存在的同一功能。 |
17. |
使用以下代碼將新的 MyWorkerThread 方法添加到 Form1 類中。 private void MyWorkerThread() { threadCounter++; if (threadCounter == 200) doneEvent.Set(); } |
18. |
假定您已將在窗體上創建的第一個按鈕命名為 CreateThreads,請將以下代碼添加到 button2_Click 事件處理程序中。 button2.Enabled = false; threadPoolCounter = 0; doneEvent = new AutoResetEvent(false); textBox2.Text = ""; int elapsedTime = Environment.TickCount; for (int i = 0; i < 200; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback(MyWaitCallBack)); } doneEvent.WaitOne(); elapsedTime = Environment.TickCount - elapsedTime; textBox2.Text = "正在創建線程:" + elapsedTime.ToString() + " msec"; button2.Enabled = true; 除了一個重要的例外情況,button2_Click 事件處理程序的代碼與 button1_Click 事件處理程序的代碼幾乎完全相同。這次,在 for 循環內部未創建任何新線程,但卻將一個回調方法添加到了 ThreadPool 類中。實際上,ThreadPool 是一個可重復使用的線程集合。使用 ThreadPool 可消除創建新線程類和設置其資源的系統開銷,從而可以使其與操作系統無縫地進行混合。因為創建工作線程是代價較高的操作,所以性能上的區別是顯著的。 因為 ThreadPool 線程是共享資源,所以您不應運行其內部的長期線程。畢竟,如果調用 QueueUserWorkItem 方法時 ThreadPool 中沒有可用的線程,則只有在線程變得可用后才能執行指定的委托。因為 ThreadPool 線程是一個可以直接運行的實際的前臺線程,所以您需要在應用程序關閉之前將其正確終止(有關正確終止線程的詳細信息,請參閱練習 2 的“正確終止創建的線程的步驟”)。 |
19. |
使用以下代碼將新的 MyWaitCallback 方法添加到 Form1 類中。 private void MyWaitCallBack(object stateInfo) { threadPoolCounter++; if (threadPoolCounter == 200) doneEvent.Set(); } 此方法將作為一個單獨的線程執行,但是 ThreadPool 對象將為您創建線程或重復使用現有的線程。當線程在 ThreadPool 中可用時,它將立即開始運行您的方法,因此無需顯式啟動該線程。 |
20. |
編譯、部署并運行該應用程序。 |
21. |
單擊 CreateThreads 按鈕,再單擊 CreateThreadPoolThreads 按鈕。該應用程序可能會花費幾秒鐘來返回結果。比較創建和運行 200 個線程與使用 200 個 ThreadPool 線程之間在計時方面的區別。 |
22. |
在該應用程序的右上角,單擊 OK(確定)退出該應用程序。 |
練習 4:更新線程內的用戶界面控件
許多開發人員常犯的一個錯誤是試圖直接從工作線程內部更新或訪問用戶界面控件。此操作將導致意外行為;通常應用程序會停止響應。要查看操作中的此問題,您還需要創建另一個將使用該錯誤方法更新用戶界面控件的 Pocket PC 應用程序。隨后,您將修改代碼以使該應用程序可以正常工作。
創建新的 Pocket PC 應用程序的步驟
1. |
在 Visual Studio 2005 中的 File(文件)菜單上,單擊 New Project(新建項目)。 |
2. |
在 Project(項目)類型下,確保選擇了 Visual C# Projects - Smart Device - Pocket PC 2003(Visual C# 項目 - 智能設備 - Pocket PC 2003)。 |
3. |
在 Templates(模板)下,確保選擇了 DeviceApplication(設備應用程序)。 |
4. |
在 Name(名稱)框中鍵入 UpdatingControls。 |
5. |
在 Location(位置)框中鍵入 C:\labs\MultithreadedLab\Lab(或者使用您首選的驅動器)。 |
6. |
確保選擇了 Create directory for solution(創建解決方案的目錄)復選框。 |
7. |
單擊 OK(確定)創建空項目。 |
8. |
通過單擊窗體中的某處并更改其 Text 屬性,將該應用程序的標題更改為 UpdatingControls。 |
9. |
為了更易于退出該應用程序,將窗體的 MinimizeBox 屬性更改為 False。 |
10. |
將兩個 Button 控件和一個 StatusBar 控件添加到該應用程序的窗體上,如下圖所示。 ![]() |
11. |
通過清除 StatusBar 控件的 Text屬性使其為空。 |
12. |
按照下圖通過更改兩個按鈕的 Text 屬性來更改其名稱。 ![]() |
13. |
通過將 StopClock 按鈕的 Enabled 屬性設置為 false 來禁用該按鈕。 |
14. |
在設計視圖中雙擊這兩個按鈕以添加單擊事件處理程序。 現在您已經結束了該應用程序用戶界面部分的創建。下一個任務是向該應用程序添加功能,以使其在狀態欄上繼續顯示當前時間。 |
15. |
通過在 Forml.cs 文件(本練習中所有代碼都將添加到此文件中)開始處的其他 using 語句下添加以下語句,為“System.Threading”命名空間創建一個別名: using System.Threading; |
16. |
將以下實例變量添加到 Form1 類中。 private Thread myThread; private bool workerThreadDone = false; |
17. |
假定您已將在窗體上創建的第一個按鈕命名為 StartClock,請將以下代碼添加到 button1_Click 事件處理程序中。 button1.Enabled = false; button2.Enabled = true; statusBar1.Text = ""; workerThreadDone = false; myThread = new Thread(new ThreadStart(MyWorkerThread)); myThread.Start(); 在您剛添加到 button1_Click 的代碼中,已經實例化并啟動了一個新的工作線程。稍后您將添加工作線程本身的功能。 |
18. |
假定您已將在窗體上創建的第一個按鈕命名為 Start Clock,請將以下代碼添加到 button2_Click 事件處理程序中。 workerThreadDone = true; button2.Enabled = false; button1.Enabled = true; 您剛添加的代碼負責終止該工作線程。它還重新啟用 Start Clock 按鈕以允許再次運行時鐘。 |
19. |
使用以下代碼將新的 MyWorkerThread 方法添加到 Form1 類中。 private void MyWorkerThread() { while (!workerThreadDone) { statusBar1.Text = DateTime.Now.ToLongDateString() + " - " + DateTime.Now.ToLongTimeString(); Thread.Sleep(0); } } |
20. |
編譯、部署并運行該應用程序。 |
21. |
單擊 StartClock 按鈕。 單擊 StartClock 按鈕幾秒種后,該應用程序將引發異常。由于您試圖從未曾創建用戶界面控件的線程內部更新用戶界面控件,因此引發了該異常。 |
22. |
通過按 SHIFT+F5 或單擊 Debug(調試)菜單上的 StopDebugging(停止調試),從 Visual Studio 2005 內部停止該調試程序。如果 Visual Studio 2005 的 Debug(調試)工具欄上的 Stop(停止)按鈕可見,您也可以使用該按鈕從 Visual Studio 2005 內部退出該應用程序。 |
即使出現異常,對于可能會不明原因地停止響應的 .NET Compact Framework version 1.0 的行為來說,這也是一個巨大的進步。若要解決該問題,您需要遵循以下原則:只有創建 UI 控件的線程才能夠安全地更新該控件。
如果需要更新工作線程內部的控件,您應該始終使用 Control.Invoke 方法。該方法執行擁有控件基礎窗口句柄的線程(換句話說,也就是創建控件的線程)上的指定的委托。.NET Compact Framework 1.0 僅支持工作線程內部用戶界面控件的同步更新。但是,.NET Compact Framework 2.0 支持用戶界面控件的異步更新。.NET Compact Framework 1.0 的另一個限制在于缺少對使用 Control.Invoke 傳遞參數的支持。使用 2.0 版則可以使用 Control.Invoke 來傳遞參數。在下一個任務中,您將探索工作線程內部用戶界面控件的同步更新。
使用 Control.Invoke 更新控件的步驟
1. |
找到 Form1.cs 源文件中 myThread 實例變量的聲明,添加以下代碼以聲明具有更新狀態欄的正確簽名的委托。 private delegate void UpdateTime(string dateTimeString); |
2. |
滾動到 Form1.cs 源文件末尾,并將以下方法(具有與將實際更新狀態欄的委托完全相同的簽名)添加到 Form1 類中。 private void UpdateTimeMethod(String dateTimeString) { statusBar1.Text = dateTimeString; } |
3. |
按照以下方法修改 MyWorkerThread 方法,以使其調用 Invoke 方法而非直接更新狀態欄本身: private void MyWorkerThread() { UpdateTime timeUpdater = new UpdateTime(UpdateTimeMethod); string currentTime; while (!workerThreadDone) { currentTime = DateTime.Now.ToLongDateString() + " - " + DateTime.Now.ToLongTimeString(); this.Invoke(timeUpdater, new object[] {currentTime}); Thread.Sleep(0); } } 正如您所看到的,在修改的代碼中,MyWorkerThread 內部的本地字符串設置為當前日期/時間值。Form1 的 Invoke 方法被調用來更新代表 MyWorkerThread 的 StatusBar 控件,但是它卻運行在主線程(創建 StatusBar 控件的線程)的上下文中。 |
4. |
編譯、部署并運行該應用程序。 |
5. |
單擊 StartClock 按鈕。正如您所看到的,現在狀態欄的日期/時間信息會不斷被更新,如下圖所示。 ![]() |
6. |
單擊 StopClock 按鈕停止工作線程,再單擊 OK(確定)關閉該應用程序。 |
若要確定何時終止工作線程,您需要添加 Thread.Join 方法。然后您可以,例如,在狀態欄上通知用戶有關工作線程終止的信息。
同步更新 UI 的步驟和死鎖風險
1. |
找到 Form1.cs 文件中的 button2_Click 事件處理程序,在 workerThreadDone = true 之后添加以下語句。 myThread.Join(); |
2. |
在 MyWorkerThread 內部,將以下語句添加到該方法的結尾。 string statusInfo = "MyWorkerThread 已終止!"; this.Invoke(timeUpdater, new object[] { statusInfo }); |
3. |
編譯、部署并運行該應用程序。 |
4. |
單擊 StartClock 按鈕。正如您所看到的,現在狀態欄正隨著日期/時間信息繼續更新。 |
5. |
通過單擊 StopClock 按鈕嘗試停止工作線程。 單擊 StopClock 按鈕后,計時器會停止,但是您沒有在狀態欄上看到剛添加的消息“MyWorkerThread 已終止!”。更糟糕的是,該應用程序停止了響應。您在 button2_Click 事件處理程序中添加 myThread.Join() 語句的事實阻止了主線程,直到工作線程終止為止。因為您還在同一事件處理程序中設置了 workerThreadDone = true,所以在完成 while 循環后工作線程仍會繼續運行以再次更新狀態欄。 請記住,Control.Invoke 是在主線程上執行的。但是,由于主線程正在等待工作線程終止而被阻止,因此 Control.Invoke 無法執行。因為 Control.Invoke 是同步操作,所以只有當通過 Control.Invoke 傳遞的委托執行完畢后才會返回。這意味工作線程無法繼續。 這是一種典型的死鎖狀態。若要解決此問題,您可以通過使用 .NET Compact Framework 2.0 中可用的異步方法來更新用戶界面控件。 |
異步更新工作線程內部 UI 的步驟
1. |
通過按 SHIFT+F5 或單擊 Debug(調試)菜單上的 StopDebugging(停止調試),從 Visual Studio 2005 內部退出該應用程序。如果 Visual Studio 2005 的 Debug(調試)工具欄上的 Stop(停止)按鈕可見,您也可以使用該按鈕從 Visual Studio 2005 內部退出該應用程序。 |
2. |
找到 MyWorkerThread 方法中的 this.Invoke(timeUpdater, new object[] { statusInfo }); 語句,將其替換成以下語句。 this.BeginInvoke(timeUpdater, new object[] { statusInfo }); 這是 Control.Invoke 的異步版本。一旦調用此方法,該工作線程將繼續運行。僅在線程切換時,主線程才會代表工作線程更新控件。通常,Control.BeginInvoke 與 Control.EndInvoke 結合使用。后一個方法檢索由傳遞的 IAsyncResult 對象代表的異步操作的返回值。然而,在此應用程序中,您卻不得不完全忽略異步操作的結果,因為在主線程能夠返回操作結果之前工作線程就將結束。更糟糕的是,將 Control.EndInvoke 添加到工作線程可能會再次導致死鎖狀態或 ObjectDisposedException。 |
3. |
編譯、部署并運行該應用程序。 |
4. |
單擊 StartClock 按鈕并等待,直到狀態欄上顯示時鐘正在更新信息。 |
5. |
單擊 StopClock 按鈕停止工作線程。 這次,您將看到工作線程在終止,并且有消息“MyWorkerThread 已終止!”顯示在狀態欄上。 |
6. |
在窗體的右上角,單擊 OK(確定)退出該應用程序。 |
到目前為止,您已經創建了一個多線程應用程序;了解了 Thread 和 ThreadPool 之間的區別。您還學習了如何從工作線程內部更新用戶界面控件。在此動手體驗的最后一部分中,您將學習如何通過使用各種同步對象來同步不同的線程。
練習 5:使用同步對象來同步線程
盡管 Windows CE 計劃程序負責計劃線程,但多線程應用程序中的所有線程似乎都將自行運行,除非對線程同步有特別的關注。特別是在多線程訪問共享數據情況下,同步訪問該數據是絕對必要的。
因為線程同步是一個非常重要的主題,所以此練習比前面的練習都要大。因此您應該將此文檔中的大部分代碼復制并粘貼到您將創建的 ThreadSynchronization 應用程序中。
創建新的 Pocket PC 應用程序的步驟
1. |
在 Visual Studio 2005 中的 File(文件)菜單上,單擊 NewProject(新建項目)。 |
2. |
在 Project(項目)類型下,確保選擇了 Visual C# Projects - Smart Device - Pocket PC 2003(Visual C# 項目 - 智能設備 - Pocket PC 2003)。 |
3. |
在 Templates(模板)下,確保選擇了 DeviceApplication(設備應用程序)。 |
4. |
在 Name(名稱)框中鍵入 ThreadSynchronization。 |
5. |
在 Location(位置)框中鍵入 C:\labs\MultithreadedLab\Lab(或者選擇您首選的驅動器)。 |
6. |
確保選擇了 Create directory for solution(創建解決方案的目錄)復選框。 |
7. |
單擊 OK(確定)創建空項目。 |
8. |
通過單擊窗體中的某處并更改其 Text 屬性,將該應用程序的標題更改為 Thread Synchronization。 |
9. |
為了更易于退出該應用程序,將窗體的 MinimizeBox 屬性更改為 False。 |
10. |
將一個 Button 控件和一個 TextBox 控件添加到該應用程序的窗體上。 |
11. |
通過清除 TextBox 控件的 Text 屬性使其全部為空,然后將其 ReadOnly 屬性設置為 true。 |
12. |
通過更改按鈕的 Text 屬性,將按鈕名稱更改為 InterlockedSample。用戶界面應如下圖所示。 ![]() |
13. |
在設計視圖中雙擊該按鈕以添加單擊事件處理程序。 現在您已經結束了該應用程序用戶界面部分的創建。下一個任務是向該應用程序添加功能。 在此練習中,您將看到線程之間對同步的需要。該應用程序會啟動兩個不同的工作線程。兩個工作線程都訪問同一變量。一個線程僅遞增該變量,而另一個線程遞減該變量。每個線程都將循環 10,000,000 次。兩個線程都執行完畢后,變量值應為零。最終結果顯示在只讀文本框中。 |
14. |
通過在 Forml.cs 文件(本練習中所有代碼都將添加到此文件中)開始處的其他 using 語句下添加以下語句,為“System.Threading”命名空間創建一個別名: using System.Threading; |
使用 interlocked 類的步驟
1. |
將以下實例變量添加到 Form1 類中。 private int counter; private bool thread1Running; private bool thread2Running; |
2. |
將以下代碼添加到 Button1_Click 事件處理程序中。 textBox1.Text = "工作線程已啟動"; button1.Enabled = false; counter = 0; Thread workerThread1 = new Thread(new ThreadStart(Thread1Function)); Thread workerThread2 = new Thread(new ThreadStart(Thread2Function)); thread1Running = true; thread2Running = true; workerThread1.Start(); workerThread2.Start(); |
3. |
通過添加以下代碼為兩個工作線程創建方法,以及一個指示兩個線程正確終止的方法。 /// <摘要> /// 重復更新實例變量的工作線程 /// </摘要> private void Thread1Function() { for (int i = 0; i < 10000000; i++) { counter++; } thread1Running = false; this.Invoke(new EventHandler(WorkerThreadsFinished)); } /// <摘要> /// 重復更新同一實例變量的工作線程 /// </摘要> private void Thread2Function() { for (int i = 0; i < 10000000; i++) { counter--; } thread2Running = false; this.Invoke(new EventHandler(WorkerThreadsFinished)); } /// <摘要> /// 工作線程之一結束時/// 所調用的委托。 /// 如果兩個線程都已結束,則處理結果會顯示給/// 用戶。 /// </摘要> private void WorkerThreadsFinished(object sender, System.EventArgs e) { if (!thread1Running && !thread2Running) { button1.Enabled = true; textBox1.Text = "計數器值 = " + counter.ToString(); } } 您應注意兩個工作線程指示它們已結束的方法:它們使用 Control.Invoke 來調用一個委托。使用 Control.Invoke 的原因在于兩個線程結束之后按鈕和文本框都已更新,并且線程本身更新了用戶界面控件。 |
4. |
編譯、部署并運行該應用程序。 |
5. |
單擊 InterlockedSample 按鈕。稍后您將看到文本框中的結果。 |
6. |
單擊 OK(確定)退出該應用程序。 通常運行兩個線程之后的最終結果為零,如下圖所示。但是,如果經常嘗試運行 Interlocked Sample,有時最終結果將是一個隨機數。即使在測試該應用程序時沒有看到意外的結果,您也應意識到在多個線程中同時更新變量的潛在危險。原因在于每個線程都以同一優先級運行。它們都獲得相等的處理器時間并執行循環。 ![]() 雖然訪問工作線程中的計數器變量看起來只需一個語句 (counter++ or counter-),但是如果您查看下圖所示的生成的 Microsoft 中間語言 (MSIL),則會發現此 C# 語句包含幾行 MSIL 代碼。突出顯示的 MSIL 代碼行有助于解釋您可能會遇到的問題。如果兩個線程都運行較長時間,則一個工作線程可能會在已讀取計數器當前值后被列在計劃之外。此時另一個工作線程會列在計劃之內,還會讀取計數器,然后修改計數器并存儲回計數器的更新值。第一個工作線程恢復運行時會準確地在以前停止之處繼續運行;它不再讀取計數器,而只是遞增原始值并將其存儲回內存中。隨即,另一個工作線程對計數器所做的全部更改都將被破壞,導致兩個工作線程終止后計數器的意外值。 ![]() 若要避免上述情況中的潛在問題,您應該保護兩個工作線程都訪問的數據。在簡單的遞增/遞減/比較操作中,您可以通過使用 Interlocked 類來保護數據。此類具有遞增/遞減和比較對象類型的方法。因為變量通過引用而非值來傳遞,所以 Interlocked 類可確保遞增/遞減操作為原子操作。 若要使用 Interlocked,只需更改兩個工作線程的代碼。 |
7. |
通過按 SHIFT+F5 或單擊 Debug(調試)菜單上的 Stop Debugging(停止調試),從 Visual Studio 2005 內部退出該應用程序。 將兩個工作線程中的遞增/遞減操作更改如下。 Interlocked.Increment(ref counter); Interlocked.Decrement(ref counter); |
8. |
編譯、部署并運行該應用程序。 |
9. |
單擊 InterlockedSample 按鈕。稍后,您將看到文本框中的結果,那時它將一直為零。 看一下下圖所示的生成的 MSIL,您將發現遞增/遞減操作過程中是否出現線程切換已無所謂,因為 Interlocked 對需要更新的變量進行了引用,并且 Interlocked 對象中的方法不會被其他線程中斷。 ![]() |
請設想以下情況。該應用程序創建了兩個工作線程,這兩個工作線程都使用另一個名為 Processing 的類的方法。這些方法正在更新 Processing 類的實例數據。兩個工作線程都訪問了 Processing 類中的所有方法。Processing 類具有兩個方法,其中每個方法都會更新循環中的計數器值。兩個函數循環的次數相同,F在您可以擴展該應用程序以實現此功能。
使用監視器進行線程同步的步驟
1. |
在窗體的右上角,單擊 OK(確定)退出該應用程序。 |
2. |
通過將另一個 Button 控件和一個 TextBox 控件添加到應用程序的窗體來擴展該應用程序的用戶界面。 |
3. |
通過清除剛添加的 TextBox 控件的 Text 屬性使其全部為空,然后將其 ReadOnly 屬性設置為 true。 |
4. |
通過更改剛添加的按鈕的 Text 屬性將其名稱更改為 MonitorSample。下圖顯示了一個示例用戶界面。 ![]() |
5. |
通過單擊 Project(項目)菜單上的 AddClass(添加類)向該項目添加一個新類。 |
6. |
在 Visual Studio installed templates(Visual Studio 安裝模板)下,確保選擇了 Class(類)。 |
7. |
在 Name(名稱)框中,將源文件的名稱由 Class1.cs 更改為 Processing.cs,如下圖所示。 ![]() |
8. |
單擊 Add(添加)將新的空類添加到 ThreadSynchronization 項目。 |
9. |
使用以下代碼替換新創建的 Processing.cs 源文件中的所有代碼(為了節約時間,您可以從此處復制所有代碼并將其粘貼到源文件中)。 using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace ThreadSynchronization { /// <摘要> /// 可以由多線程訪問的 Processing 類。 /// 此類說明監視器的使用情況。 /// </摘要> public class Processing { private const int nrLoops = 10; private int counter1; private int counter2; /// <摘要> /// Processing 類的構造函數。 /// </摘要> public Processing() { counter1 = 0; counter2 = 0; } /// <摘要> /// 訪問監視器保護的某些數據。 /// </摘要> public void Function1() { // Monitor.Enter(this); for (int i = 0; i < nrLoops; i++) { int localCounter = counter1; Thread.Sleep(0); localCounter++; Thread.Sleep(0); counter1 = localCounter; Thread.Sleep(0); } // Monitor.Exit(this); } /// <摘要> /// 訪問監視器保護的某些數據。 /// </摘要> public void Function2() { // Monitor.Enter(this); for (int i = 0; i < nrLoops; i++) { int localCounter = counter2; localCounter++; counter2 = localCounter; } // Monitor.Exit(this); } /// <摘要> /// 返回多個線程訪問過的 /// 兩個計數器變量之間的差異 /// </摘要> public int Counter { get { return counter1 - counter2; } } } } Processing 類的功能非常簡單。它包含兩個不同的函數,每個都以常數次遞增一個變量。該類還包含一個只讀屬性,返回兩個計數器之間的差異。您也許還注意到,您在 Function1 和 Function2 中都使用了本地變量來更新實例變量,并且 Function1 包含很多對 Thread.Sleep 方法的調用。這些都是您在實際的應用程序中不會使用的功能,此代碼僅僅是為了模擬幾個線程試圖獨立地更新相同變量時發生的情況。 另外,請注意 Processing 類的源代碼包含很多對名為 Monitor 的類的方法的調用。正如您在源代碼中所看到的,這些方法設置為注釋語句。請將其留下以備后用。 若要使用此類,您現在需要在 Form1.cs 源文件中創建兩個不同的工作線程。每個線程都將調用 Processing 類中的兩個函數,但調用的順序相反。線程結束運行后,您將通過調用 Processing.Counter 屬性并將其顯示在文本框中來查詢最終結果。 |
10. |
將幾個實例變量添加到源文件 Form1.cs 中。 private Processing processing; private bool workerThread1Done; private bool workerThread2Done; |
11. |
通過雙擊設計視圖中的 MonitorSample 按鈕來為其添加單擊事件處理程序。將以下代碼添加到 button2_Click 事件處理程序中。 button2.Enabled = false; textBox2.Text = "正在運行監視器樣本"; processing = new Processing(); workerThread1Done = false; workerThread2Done = false; Thread thread1 = new Thread(new ThreadStart(Thread1Monitor)); Thread thread2 = new Thread(new ThreadStart(Thread2Monitor)); thread1.Start(); thread2.Start(); |
12. |
為兩個工作線程創建方法,其中一個方法通過添加以下代碼來指示兩個線程的正確終止。 private void Thread1Monitor() { processing.Function1(); processing.Function2(); Thread.CurrentThread.Priority = ThreadPriority.AboveNormal; workerThread1Done = true; this.Invoke(new EventHandler(WorkerThreadsDone)); } private void Thread2Monitor() { processing.Function1(); processing.Function2(); Thread.CurrentThread.Priority = ThreadPriority.AboveNormal; workerThread2Done = true; this.Invoke(new EventHandler(WorkerThreadsDone)); } private void WorkerThreadsDone(object sender, System.EventArgs e) { if (workerThread1Done && workerThread2Done) { textBox2.Text = "處理結果:" + processing.Counter.ToString(); button2.Enabled = true; } } |
13. |
編譯、部署并運行該應用程序。 |
14. |
單擊 MonitorSample 按鈕和 InterlockedSample 按鈕,查看兩個工作線程結束其作業之后顯示的結果,如下圖所示。 ![]() 這些工作線程隨時可以完全自由地更新數據。它們都會以相同順序調用 Processing.Function1 和 Processing.Function2。兩個線程都結束后,您可以要求 Processing 類顯示結果(只需返回 counter1 - counter2)。因為兩個計數器在 Processing 類中具有相同的遞增數,所以正確的返回值應該為零,但是不考慮同步時,則可能會返回意外的結果(如您剛才所經歷的那樣)。 結果是意外的,因為線程未同步運行。也許一個線程正在更新計數器變量時另一個線程突然運行。該行為會導致覆蓋原始值,因為原始線程預定為在此時退出,盡管仍有工作要完成,該行為是完全隨機的。 若要解決此問題,您需要使用 Monitor 類來保護 Processing 類中的數據,使其不會由多個線程同時訪問。 除了 Monitor,還可以將另一個帶有比較功能的類用于同步:Mutex 類。由于時間的限制,在此動手體驗中您將不會使用 Mutex,但了解這兩個類是很重要的。Monitor 和 Mutex 都會保護一次只能由一個線程執行的代碼的區域。使用這些同步對象給作為開發人員的您增加了一些工作。在訪問需要保護的代碼區域之前,您將調用 Mutex.WaitOne 或 Monitor.Enter(取決于您使用的同步對象)。這些方法將使您在沒有其他線程執行特定代碼時可以立即訪問受保護的代碼區域。然而,如果其他線程已經執行了該代碼,Mutex.WaitOne 和 Monitor.Enter 將阻止請求線程,直到當前執行受保護的代碼區域的線程分別調用 Mutex.Release 或 Monitor.Exit 為止。省略釋放先前獲得的 Monitor 或 Mutex 對象可能會導致意外結果(包括死鎖)。因此,正確使用這些對象是極其重要的。在受保護的代碼區域可能捕獲異常的情況下,您應確保在 finally 塊中添加異常處理并釋放同步對象。 |
15. |
在窗體的右上角,單擊 OK(確定)退出該應用程序。 |
16. |
現在您需要在 Visual Studio 2005 內部編輯源文件 Processing.cs。只需找到源文件中出現的所有 Monitor.Enter 和 Monitor.Exit 并將它們取消注釋(總共應該出現四次)。 |
17. |
編譯、部署并運行該應用程序。 |
18. |
單擊 MonitorSample 按鈕和 InterlockedSample 按鈕,查看兩個工作線程結束其作業之后顯示的結果。 |
這次,您將看到顯示的結果始終為零。添加對 Monitor 類的調用將確保一次只有一個線程訪問 Processing.Function1 方法和 Processing.Function2 方法。
在托管的多線程環境中,事件是可以用于以信號形式通知線程發生何種情況的對象。在此文檔的上下文中,事件是同步對象 - 請不要與和事件處理程序相關聯的 UI 事件混淆。在 .NET Compact Framework 中,可以使用兩個不同類型的事件 AutoResetEvent 和 ManualResetEvent。它們的主要區別在于 AutoResetEvent 事件已設置,并在線程等待該事件時立即再次重置。而 ManualResetEvent 事件將保持設置,直到再次顯式重置為止。若要顯示兩個不同類型的事件在操作中的區別,您需要將一些代碼添加到在此練習使用的應用程序中。
比較 AutoResetEvent 和 ManualResetEvent 的步驟
1. |
在窗體的右上角,單擊 OK(確定)退出該應用程序。 |
2. |
通過將另外兩個 Button 控件、另一個 TextBox 控件和一個 CheckBox 控件添加到應用程序的窗體,最后一次擴展該應用程序的用戶界面。 |
3. |
通過清除剛添加的 TextBox 控件的 Text 屬性使其全部為空,然后將其 ReadOnly 屬性設置為 true。 |
4. |
通過更改剛添加的按鈕的 Text 屬性,將其名稱分別更改為 StartEventSample 和 SetEvent。 |
5. |
通過更改這兩個按鈕的 Name 屬性更改其變量名。將 Start Event Sample 按鈕命名為 eventThreadButton。將 SetEvent 按鈕命名為 setEventButton。界面應如下圖所示。 ![]() |
6. |
通過將名為 SetEvent 的按鈕的 Enabled 屬性設置為 false 來禁用該按鈕。 在此最后一項任務中,您將創建一個在設置事件時會激活的工作線程。您將通過單擊 SetEvent 按鈕從用戶界面設置事件。使用 CheckBox 控件,您可以在 AutoResetEvent 和 ManualResetEvent 之間進行選擇并研究其行為的區別。每次設置事件時,工作線程執行的次數都會在文本框中顯示。 請注意,您從 UI 線程內部更新所有用戶界面控件。從工作線程直接更新 UI 控件將導致意外結果,除非使用了 Control.Invoke。 您將添加的代碼具有某種可區分 AutoResetEvent 和 ManualResetEvent 的邏輯。還具有某種可啟用/禁用相關按鈕的功能。 |
7. |
將以下實例變量添加到 Form1 類中。 private int eventWorkerThreadCounter = 0; private bool eventWorkerThreadDone = true; private Thread eventWorkerThread = null; private AutoResetEvent runOnceEvent = null; private ManualResetEvent runManyEvent = null; private bool useAutoResetEvent = false; private bool eventSampleRunning = false; |
8. |
通過雙擊設計視圖中的 Start Event Sample 按鈕來為其添加單擊事件處理程序。 private void eventThreadButton_Click(object sender, EventArgs e) { } |
9. |
將以下代碼添加到剛創建的按鈕單擊處理程序(因為該按鈕單擊處理程序非常大,所以您也許要從本文檔復制該代碼并將其粘貼到單擊處理程序中)。 if (eventSampleRunning) { // 用戶請求停止演示,因此確保 // 正確終止該工作線程。 setEventButton.Enabled = false; eventWorkerThreadDone = true; // 因為該工作線程在啟動處理之前 // 繼續等待事件,所以若要終止工作線程, // 您需要最后一次設置事件。 if (useAutoResetEvent) { runOnceEvent.Set(); } else { runManyEvent.Set(); } eventWorkerThread.Join(); textBox3.Text = ""; checkBox1.Enabled = true; eventThreadButton.Text = "Start Event Sample"; setEventButton.Text = "Set Event"; eventSampleRunning = false; } else { // 用戶請求啟動演示,因此創建 // 一個僅計數其循環次數的 // 工作線程。 eventWorkerThreadDone = false; eventWorkerThreadCounter = 0; checkBox1.Enabled = false; if (useAutoResetEvent) { runOnceEvent = new AutoResetEvent(false); } else { runManyEvent = new ManualResetEvent(false); } eventWorkerThread = new Thread(new ThreadStart(MyEventWorkerThread)); eventWorkerThread.Start(); setEventButton.Enabled = true; eventThreadButton.Text = "Stop Event Sample"; eventSampleRunning = true; } 您剛已添加了某種功能,以正確創建一個工作線程并實例化一個新的 AutoResetEvent 事件或一個新的 ManualResetEvent 事件(根據復選框的狀態)。因為 Pocket PC 的屏幕尺寸有限,所以在結束示例之后您需要重復使用同一按鈕來正確終止工作線程。 |
10. |
通過雙擊設計視圖中的 SetEvent 按鈕來為其添加單擊事件處理程序。 private void eventThreadButton_Click(object sender, EventArgs e) { } |
11. |
將以下代碼添加到剛創建的按鈕單擊處理程序(或者從本文檔粘貼過去): textBox3.Text = "工作線程循環:" + eventWorkerThreadCounter.ToString(); if (useAutoResetEvent) { runOnceEvent.Set(); } else { if (setEventButton.Text == "Set Event") { setEventButton.Text = "Reset Event"; runManyEvent.Set(); } else { setEventButton.Text = "Set Event"; runManyEvent.Reset(); } } 你剛已添加了某種功能,以通過單擊按鈕設置事件。如果是 ManualResetEvent 事件,則將重復使用該按鈕以重置該事件。 您需要另一個 UI 控件事件處理程序,以監視添加的復選框的狀態更改。 |
12. |
單擊設計視圖中的復選框。 |
13. |
在 Properties(屬性)窗口中,單擊工具欄上的 Events(事件)按鈕。 雙擊 CheckStateChanged 事件。 |
14. |
將以下代碼添加到 CheckStateChanged 處理程序。 useAutoResetEvent = checkBox1.Checked; |
15. |
通過將以下代碼添加到 CheckStateChanged 方法下來創建工作線程。 private void MyEventWorkerThread() { WaitHandle nextEvent = useAutoResetEvent ? (WaitHandle)runOnceEvent : (WaitHandle)runManyEvent; Thread.CurrentThread.Priority = ThreadPriority.BelowNormal; while (!eventWorkerThreadDone) { nextEvent.WaitOne(); if (! eventWorkerThreadDone) { eventWorkerThreadCounter++; Thread.Sleep(10); } } } 工作線程的第一個語句將 WaitHandle 對象分配給 runOnceEvent 事件(AutoResetEvent 的一個實例化)或 runManyEvent 事件(ManualResetEvent 的一個實例化)。AutoResetEvent 和 ManualResetEvent 都從 WaitHandle 派生而來。利用這一事實,您可以只在工作線程中等待對 WaitHandle 對象的設置。要是能夠將參數傳遞給工作線程,則將其傳遞給 WaitHandle 對象,使工作線程完全不知道它所等待的事件的類型。若要模擬該操作,現在您需要使用本地 WaitHandle 對象并將其分配給正確的實例變量。 該線程本身極其簡單。每次它從 WaitOne 方法返回(在設置事件時發生),計數器都遞增并且線程都休眠 10 毫秒。如果您在用戶界面中選擇 AutoResetEvent 事件,則在每次單擊 SetEvent 按鈕時工作線程都會運行一次會變得很明顯。如果通過清除復選框更改為 ManualResetEvent 事件,然后再次啟動工作線程,則行為將變得不同。在單擊 SetEvent 按鈕之后,工作線程將開始運行,并將持續下去,直到單擊 ResetEvent 按鈕為止。 |
16. |
編譯、部署并運行該應用程序。 該應用程序將如下圖所示。 ![]() |
17. |
單擊 StartEvent Sample 按鈕。 |
18. |
單擊 SetEvent 按鈕,并注意工作線程的行為。 |
19. |
單擊 StopEventSample 按鈕,然后更改復選框的狀態。 |
20. |
再次啟動示例,并注意單擊 SetEvent 按鈕幾次后行為的變化,如下圖所示。 ![]() |
21. |
再次停止示例,然后單擊 OK(確定)退出該應用程序。 |
祝賀您!現在您已經完成了“使用 .NET Compact Framework 2.0 開發多線程應用程序”這一體驗。
此任務說明了如何終止在設備或仿真器上運行的應用程序。如果在沒有連接調試器的情況下啟動了應用程序,并且需要終止該應用程序以便可以部署新的應用程序副本,則會用到此任務。將通過在 Visual Studio 中使用遠程進程查看器這一遠程工具來終止應用程序。
需要知道可執行文件的名稱才能終止進程。大多數情況下,此名稱就是 Visual Studio 項目的名稱。如果您不確定可執行文件的名稱,則可以在項目屬性中查找。
終止在設備或仿真器上運行的應用程序的步驟
1. |
從 Visual Studio 中,選擇 Project(項目),然后選擇 xxx Properties(xxx 屬性),其中 xxx 代表當前項目的名稱。 |
2. |
注意 AssemblyName(程序集名稱)字段中的值。此值是可執行文件在設備或仿真器上運行時將使用的名稱。 |
3. |
關閉 Properties(屬性)對話框。 現在,您就可以終止進程了。 |
4. |
從 Start(開始)菜單中,單擊 Start > Microsoft Visual Studio 2005 > Visual Studio Remote Tools > Remote Process Viewer(開始 > Microsoft Visual Studio 2005 > Visual Studio 遠程工具 > 遠程進程查看器)。 |
5. |
收到 Select a Windows CE Device(選擇一個 Windows CE 設備)對話框提示后,選擇正在運行該應用程序的仿真器或設備(如下圖所示),然后單擊 OK(確定)。 ![]() |
6. |
連接到仿真器或設備之后,在 RemoteProcessViewer(遠程進程查看器)的頂部窗格中找到您要終止的應用程序,如下圖所示。 ![]() 可能需要加寬 Process(進程)列(最左邊的列)以完全顯示出進程名稱。 |
7. |
單擊進程名稱以選擇進程。 |
8. |
要終止進程,請從 RemoteProcess Viewer(遠程進程查看器)菜單中選擇 File > Terminate Process(文件 > 終止進程)。 注意:請確保在單擊 Terminate Process(終止進程)之前選擇了正確的進程。終止不正確的進程可能會致使設備或仿真器不可用,這樣就必須將其復位才能再次使用。 |
9. |
通過在 Remote Process Viewer(遠程進程查看器)菜單上選擇 Target > Refresh(目標 > 刷新)并再次滾動瀏覽頂部窗格,來驗證進程是否已終止。如果該應用程序名仍然存在,則說明進程未被終止,您需要重復以上步驟。 |
注意:大多數進程只需執行一次操作即可終止;但是,根據應用程序狀態的不同,偶爾會需要執行兩次操作才能終止。
總結
在此體驗中,您執行了以下練習。
• |
使用 .NET Compact Framework 2.0 創建多線程應用程序 |
• |
修改多線程應用程序 |
• |
檢查 Thread 和 ThreadPool |
• |
更新線程內的用戶界面控件 |
• |
使用同步對象來同步線程 |
在此體驗中,您創建了一些應用程序來探索 .NET Compact Framework 2.0 的多線程功能。您學習了如何正確終止多線程應用程序。您還學會了如何從工作線程內部更新用戶界面控件。最后,您學習了使用同步對象來同步線程,以安全地更新共享數據。