适用于:
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 方法是非常重要的,否则该线程将不会运行。现在是再次运行该应用程序的时候了,注意其与先前运行的首版之间的不同。 |
|
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 中存在对前台线程和后台线程的区分。现在,您将体验两者行为中的不同之处。为此,您需要再次启动应用程序中的工作线程。
正确终止创建的线程的步骤
|
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 类具有两个方法,其中每个方法都会更新循环中的计数器值。两个函数循环的次数相同。现在您可以扩展该应用程序以实现此功能。
使用监视器进行线程同步的步骤
|
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 的多线程功能。您学习了如何正确终止多线程应用程序。您还学会了如何从工作线程内部更新用户界面控件。最后,您学习了使用同步对象来同步线程,以安全地更新共享数据。


