多线程和用户界面基础知识
构建一个不稳定的应用程序
您们中的许多人可能已经熟悉从后台线程与 UI 交互的相关问题,但作为一种复习,让我们快速回顾一下。考虑下面的代码示例。
class MyForm : Form{
ListBox lbData ;
MyForm() {
InitializeComponent(); // Create form controls
Work1_(); // Call Work1_ on the current thread
}
void Work1_(){
StreamReader rdr1 = new StreamReader(@"\My Documents\DataFile.dat");
string line = rdr1.ReadLine();
while(line != null) {
lbData.Items.Add(line); // Populates the list box as expected
line = rdr1.ReadLine();
}
}
}
这是一个非常简单的示例,但是它表示了智能设备开发人员所面临的共同问题:需要用数据填充应用程序 UI,检索这些数据可能会非常耗时。在该示例中,应用程序创建了一个包含有列表框的窗体,然后调用函数 Work1_ 来用某个文件的内容填充列表框。
如果该文件很小,那么毫无意外,该应用程序会运行的非常好。但是,如果读取数据的过程所花时间过长,那么呈现给用户的应用程序可能会无响应甚至会冻结。如果将应用程序修改为从低带宽的无线连接中读取数据,应用程序的无响应性则更需要关注。
我们必须确保开发人员执行一项冗长的任务时 UI 要保持响应性的一种方法是,将该任务转移给一条后台线程。这不会使实际任务的运行速度更快,但是通过长时间运行的任务在后台运行期间允许应用程序的其他部分继续进行,它确实提供了一种响应性更好的用户体验。
通过使用 Thread 类和 ThreadStart 委托在后台线程中执行 Work1_,我们可以轻松地将应用程序修改为使用多线程。
class MyForm : Form{
ListBox lbData ;
MyForm() {
InitializeComponent(); // Create form controls
Thread t = new Thread(new ThreadStart(Work1_));
t.Start() ; // Runs Work1_ on a background thread
}
void Work1_(){
StreamReader rdr1 = new StreamReader(@"\My Documents\DataFile.dat");
string line = rdr1.ReadLine();
while(line != null) {
lbData.Items.Add(line); // This line is unstable
line = rdr1.ReadLine();
}
}
}
好消息是,长时间运行的任务现在在后台运行,因此不会延时或者冻结 UI。坏消息是,在引入多线程之前很稳定的应用程序现在好像会随机发生崩溃。实际上,程序很不稳定,所以我们不可能成功地部署它。
问题在于 Microsoft .NET 中所有的 Microsoft Windows 窗体控件都有所谓的线程关系,意思是说,它们的属性和方法只能由运行在创建该控件的同一个线程上的代码调用。对于本例的情况,lbData 是在主应用程序线程上创建的,但却是从一个后台线程调用 lbData.Items.Add 的。从后台线程调用 lbData.Items.Add 会导致数据损坏。
注有关 Windows 窗体控件和多线程需要特殊考虑的具体原因,请参阅 Chris Sells 的文章 Safe, Simple Multithreading in WinForms。本文的目标是 .NET Framework 完全版,因此该文章提供的一些解决方案不适用于 .NET Compact Framework,但 Chris 对问题的描述极为不错。
亡羊补牢
为了使我们的应用程序重新稳定,我们需要修改代码,这样所有与列表框的交互都会在主应用程序线程上发生。通过使用列表框上的 Invoke 方法,我们可以修改代码。Invoke 方法由 System.Windows.Forms.Control 基类提供,因此由所有的 Windows 窗体控件公开。Control.Invoke 方法在最初创建控件的线程上运行某个委托,允许该委托安全地与控件交互。
注.NET Framework 实现可以运行任何委托,与此不同,Control.Invoke 的 .NET Compact Framework 实现只支持 EventHandler 委托。
class MyForm : Form{
ListBox lbData ;
MyForm() {
InitializeComponent(); // Create form controls
Thread t = new Thread(new ThreadStart(Work1_));
t.Start() ; // Runs Work1_ on a background thread
}
private Queue qData = new Queue(); // Visible to all member functions on all threads
void Work1_(){
// Wrap AddItem in delegate
EventHandler eh = new EventHandler(AddItem);
StreamReader rdr1 = new StreamReader(@"\My Documents\DataFile.dat");
string line = rdr1.ReadLine();
while(line != null) {
lock(qData){ // Synchronize queue acess
qData.Enqueue(line); // Store line value in queue
}
lbData.Invoke(eh); // Transfer control to thread that created lbData
line = rdr1.ReadLine();
}
}
void AddItem(object o, EventArgs e)
{
string line = null;
lock(qData){ // Synchronize queue acess
line = (string)qData(); // Get data from queue
}
lbData.Items.Add(line); // Update list box
}
克服局限性
对于简单的线程方案,Control.Invoke 的 .NET Compact Framework 实现很适用,但与 .NET Framework 实现相比却具有明显的局限性。
传递参数
首先,.NET Framework 提供了 Control.Invoke 的一种重载,它接受一个对象数组。用该对象数组将参数传递给执行的委托。
通过使用 .NET Framework 中的 Control.Invoke 重载,我们不再需要使用队列或者任何其他的数据结构在线程之间共享数据。数据可以只是作为委托调用的一部分而传递,明显地简化了在后台与 UI 线程之间的数据传递。
使用 Control.Invoke 重载生成下面 Work1_ 与 AddItem 的实现。
void Work1_(){
// Wrap AddItem in delegate
EventHandler eh = new EventHandler(AddItem);
StreamReader rdr1 = new StreamReader(@"\My Documents\DataFile.dat");
string line = rdr1.ReadLine();
while(line != null) {
lbData.Invoke(eh, new object[]{line, EventArgs.Empty}); // Pass to AddItem
line = rdr1.ReadLine();
}
}
// o receives the reference to line, e receives EventArgs.Empty
void AddItem(object o, EventArgs e)
{
string line = (string) o; // Upcast o
lbData.Items.Add(line); // Add to list box
}
异步执行
另一个主要不同点是,台式计算机支持 Control.BeginInvoke,这样委托可以异步执行。在我们的应用程序中,每次调用 lbData.Invoke 时,后台线程就挂起执行,直到 AddItem 方法结束。结果是,循环的每次迭代中应用程序被迫导致一个线程上下文切换。
一般情况下,我们希望将线程上下文切换降低到最低限度,因为执行它的成本相当高;首选的做法是允许操作系统选择何时发出线程上下文切换。用 .NET Framework 中的 Control.BeginInvoke 替代 Control.Invoke 调用消除了这种不得已的线程上下文切换,并允许后台线程继续处理,直到操作系统决定执行一个线程上下文切换并运行委托。
为了更新 Work1_ 方法来异步运行 AddItem 委托,我们只需使用 lbData.BeginInvoke 替代对 lbData.Invoke 的调用。
lbData.BeginInvoke(eh, new object[]{line, EventArgs.Empty});
返回页首
构建更好的类
我们在构建多线程设备应用程序时,.NET Compact Framework Control 类缺乏支持传递参数以及异步执行增加了复杂性并降低了效率。我发现这种不支持是一个特别重要的问题,因为智能设备应用程序一般都使用多线程。同时,智能设备的资源往往有限,这使得简单、有效的多线程非常重要。
因为我们没有使用 .NET Compact Framework 源代码,因此我们不能合理地向 .NET Compact Framework Control 类添加对参数和异步委托执行的支持。但是,我们可以构建一个提供这些功能的新类。我将该类称为 UISafeInvoker。
注本月专栏随附的下载包括 UISafeInvoker 的完整源代码以及一个演示其用法的应用程序。
一言以蔽之,UISafeInvoker 是一个与线程有关的 .NET Compact Framework 类,它提供了行为与 .NET Framework Control 类的 Invoke 与 BeginInvoke 方法类似的 Invoke 与 BeginInvoke 方法。虽然不是 Control 类的一部分,但 UISafeInvoker.Invoke 与 UISafeInvoker.BeginInvoke 方法的使用却非常简单。
注本专栏的作者提供了 UISafeInvoker 的代码示例。Microsoft 不提供对这些代码的支持,对该类的使用也没有明确或暗示的任何担保。
与可以执行任何种类委托的台式机的 Control.Invoke 与 BeginInvoke 方法不同,UISafeInvoker 和 .NET Compact Framework Control.Invoke 与 BeginInvoke 方法一样,只支持 EventHandler 委托。因为 UISafeInvoker 仅支持一种委托类型,因此不需要使用对象数组来传递参数。相反,Invoke 与 BeginInvoke 接受直接传递给 EventHandler 委托中对应参数的对象和 EventArgs 参数。这里是每种方法的签名。
void Invoke(EventHandler eh, object obj, EventArgs eArgs);
IAsyncResult BeginInvoke(EventHandler eh, object obj, EventArgs eArgs);
使用 UISafeInvoker 就是简单地在 Form 类中声明一个引用,并在 Form 构造函数中创建一个实例。创建之后,UISafeInvoker 就内部跟踪创建它的线程,因此 Invoke 和 BeginInvoke 方法可以在同一个线程上运行期望的委托。作为 Form 构造函数的一部分而创建 UISafeInvoker,并且是在与所有窗体控件相同的线程上创建;因此,Invoke 或者 BeginInvoke 方法运行的任何委托都可以安全地更新 UI 控件。
这里是使用 UISafeInvoker 更新后的测试应用程序。
class MyForm : Form{
ListBox lbData ;
UISafeInvoker invoker ; // Declare UISafeInvoker
MyForm() {
InitializeComponent();
invoker = new UISafeInvoker(); // Create UISafeInvoker on main UI thread
Thread t = new Thread(new ThreadStart(Work1_));
t.Start() ; // Runs Work1_ on a background thread
}
void Work1_(){
// Wrap AddItem in delegate
EventHandler eh = new EventHandler(AddItem);
StreamReader rdr1 = new StreamReader(@"\My Documents\DataFile.dat");
string line = rdr1.ReadLine();
while(line != null) {
invoker.BeginInvoke(eh, line, EventArgs.Empty); // Pass to AddItem
line = rdr1.ReadLine();
}
}
// o receives the reference to line, e receives EventArgs.Empty
void AddItem(object o, EventArgs e)
{
string line = (string) o; // Upcast o
lbData.Items.Add(line); // Add to Listbox
}
}
对于 UISafeInvoker,我们的 .NET Compact Framework 应用程序已经克服了 .NET Compact Framework Control.Invoke 方法的局限性,现在可以提供简单、有效的线程内通信了。这种通信类似于 .NET Framework 中的通信,不需要额外的数据结构或者复杂的编码。
使用窗口消息
在内部,UISafeInvoker 非常简单,因为它仅完成两件事情:跟踪创建它的线程并提供一种在线程之间传输数据的可靠方法。
这种解决方案 — 虽然听起来有些陈旧 — 是基于窗口消息的。Windows 操作系统创建的所有窗口都有一个消息队列。应用程序可以通过使用 Microsoft Win32_ SDK 函数 SendMessage 和 PostMessage 将消息放置到该队列中。这些函数允许应用程序传递一个标识执行操作的整数标志和两个过去被称为 WParam 与 LParam 的消息定义数据值。
构建一个不稳定的应用程序
您们中的许多人可能已经熟悉从后台线程与 UI 交互的相关问题,但作为一种复习,让我们快速回顾一下。考虑下面的代码示例。
class MyForm : Form{
ListBox lbData ;
MyForm() {
InitializeComponent(); // Create form controls
Work1_(); // Call Work1_ on the current thread
}
void Work1_(){
StreamReader rdr1 = new StreamReader(@"\My Documents\DataFile.dat");
string line = rdr1.ReadLine();
while(line != null) {
lbData.Items.Add(line); // Populates the list box as expected
line = rdr1.ReadLine();
}
}
}
这是一个非常简单的示例,但是它表示了智能设备开发人员所面临的共同问题:需要用数据填充应用程序 UI,检索这些数据可能会非常耗时。在该示例中,应用程序创建了一个包含有列表框的窗体,然后调用函数 Work1_ 来用某个文件的内容填充列表框。
如果该文件很小,那么毫无意外,该应用程序会运行的非常好。但是,如果读取数据的过程所花时间过长,那么呈现给用户的应用程序可能会无响应甚至会冻结。如果将应用程序修改为从低带宽的无线连接中读取数据,应用程序的无响应性则更需要关注。
我们必须确保开发人员执行一项冗长的任务时 UI 要保持响应性的一种方法是,将该任务转移给一条后台线程。这不会使实际任务的运行速度更快,但是通过长时间运行的任务在后台运行期间允许应用程序的其他部分继续进行,它确实提供了一种响应性更好的用户体验。
通过使用 Thread 类和 ThreadStart 委托在后台线程中执行 Work1_,我们可以轻松地将应用程序修改为使用多线程。
class MyForm : Form{
ListBox lbData ;
MyForm() {
InitializeComponent(); // Create form controls
Thread t = new Thread(new ThreadStart(Work1_));
t.Start() ; // Runs Work1_ on a background thread
}
void Work1_(){
StreamReader rdr1 = new StreamReader(@"\My Documents\DataFile.dat");
string line = rdr1.ReadLine();
while(line != null) {
lbData.Items.Add(line); // This line is unstable
line = rdr1.ReadLine();
}
}
}
好消息是,长时间运行的任务现在在后台运行,因此不会延时或者冻结 UI。坏消息是,在引入多线程之前很稳定的应用程序现在好像会随机发生崩溃。实际上,程序很不稳定,所以我们不可能成功地部署它。
问题在于 Microsoft .NET 中所有的 Microsoft Windows 窗体控件都有所谓的线程关系,意思是说,它们的属性和方法只能由运行在创建该控件的同一个线程上的代码调用。对于本例的情况,lbData 是在主应用程序线程上创建的,但却是从一个后台线程调用 lbData.Items.Add 的。从后台线程调用 lbData.Items.Add 会导致数据损坏。
注有关 Windows 窗体控件和多线程需要特殊考虑的具体原因,请参阅 Chris Sells 的文章 Safe, Simple Multithreading in WinForms。本文的目标是 .NET Framework 完全版,因此该文章提供的一些解决方案不适用于 .NET Compact Framework,但 Chris 对问题的描述极为不错。
亡羊补牢
为了使我们的应用程序重新稳定,我们需要修改代码,这样所有与列表框的交互都会在主应用程序线程上发生。通过使用列表框上的 Invoke 方法,我们可以修改代码。Invoke 方法由 System.Windows.Forms.Control 基类提供,因此由所有的 Windows 窗体控件公开。Control.Invoke 方法在最初创建控件的线程上运行某个委托,允许该委托安全地与控件交互。
注.NET Framework 实现可以运行任何委托,与此不同,Control.Invoke 的 .NET Compact Framework 实现只支持 EventHandler 委托。
class MyForm : Form{
ListBox lbData ;
MyForm() {
InitializeComponent(); // Create form controls
Thread t = new Thread(new ThreadStart(Work1_));
t.Start() ; // Runs Work1_ on a background thread
}
private Queue qData = new Queue(); // Visible to all member functions on all threads
void Work1_(){
// Wrap AddItem in delegate
EventHandler eh = new EventHandler(AddItem);
StreamReader rdr1 = new StreamReader(@"\My Documents\DataFile.dat");
string line = rdr1.ReadLine();
while(line != null) {
lock(qData){ // Synchronize queue acess
qData.Enqueue(line); // Store line value in queue
}
lbData.Invoke(eh); // Transfer control to thread that created lbData
line = rdr1.ReadLine();
}
}
void AddItem(object o, EventArgs e)
{
string line = null;
lock(qData){ // Synchronize queue acess
line = (string)qData(); // Get data from queue
}
lbData.Items.Add(line); // Update list box
}
}
克服局限性
对于简单的线程方案,Control.Invoke 的 .NET Compact Framework 实现很适用,但与 .NET Framework 实现相比却具有明显的局限性。
传递参数
首先,.NET Framework 提供了 Control.Invoke 的一种重载,它接受一个对象数组。用该对象数组将参数传递给执行的委托。
通过使用 .NET Framework 中的 Control.Invoke 重载,我们不再需要使用队列或者任何其他的数据结构在线程之间共享数据。数据可以只是作为委托调用的一部分而传递,明显地简化了在后台与 UI 线程之间的数据传递。
使用 Control.Invoke 重载生成下面 Work1_ 与 AddItem 的实现。
void Work1_(){
// Wrap AddItem in delegate
EventHandler eh = new EventHandler(AddItem);
StreamReader rdr1 = new StreamReader(@"\My Documents\DataFile.dat");
string line = rdr1.ReadLine();
while(line != null) {
lbData.Invoke(eh, new object[]{line, EventArgs.Empty}); // Pass to AddItem
line = rdr1.ReadLine();
}
}
// o receives the reference to line, e receives EventArgs.Empty
void AddItem(object o, EventArgs e)
{
string line = (string) o; // Upcast o
lbData.Items.Add(line); // Add to list box
}
异步执行
另一个主要不同点是,台式计算机支持 Control.BeginInvoke,这样委托可以异步执行。在我们的应用程序中,每次调用 lbData.Invoke 时,后台线程就挂起执行,直到 AddItem 方法结束。结果是,循环的每次迭代中应用程序被迫导致一个线程上下文切换。
一般情况下,我们希望将线程上下文切换降低到最低限度,因为执行它的成本相当高;首选的做法是允许操作系统选择何时发出线程上下文切换。用 .NET Framework 中的 Control.BeginInvoke 替代 Control.Invoke 调用消除了这种不得已的线程上下文切换,并允许后台线程继续处理,直到操作系统决定执行一个线程上下文切换并运行委托。
为了更新 Work1_ 方法来异步运行 AddItem 委托,我们只需使用 lbData.BeginInvoke 替代对 lbData.Invoke 的调用。
lbData.BeginInvoke(eh, new object[]{line, EventArgs.Empty});
返回页首
构建更好的类
我们在构建多线程设备应用程序时,.NET Compact Framework Control 类缺乏支持传递参数以及异步执行增加了复杂性并降低了效率。我发现这种不支持是一个特别重要的问题,因为智能设备应用程序一般都使用多线程。同时,智能设备的资源往往有限,这使得简单、有效的多线程非常重要。
因为我们没有使用 .NET Compact Framework 源代码,因此我们不能合理地向 .NET Compact Framework Control 类添加对参数和异步委托执行的支持。但是,我们可以构建一个提供这些功能的新类。我将该类称为 UISafeInvoker。
注本月专栏随附的下载包括 UISafeInvoker 的完整源代码以及一个演示其用法的应用程序。
一言以蔽之,UISafeInvoker 是一个与线程有关的 .NET Compact Framework 类,它提供了行为与 .NET Framework Control 类的 Invoke 与 BeginInvoke 方法类似的 Invoke 与 BeginInvoke 方法。虽然不是 Control 类的一部分,但 UISafeInvoker.Invoke 与 UISafeInvoker.BeginInvoke 方法的使用却非常简单。
注本专栏的作者提供了 UISafeInvoker 的代码示例。Microsoft 不提供对这些代码的支持,对该类的使用也没有明确或暗示的任何担保。
与可以执行任何种类委托的台式机的 Control.Invoke 与 BeginInvoke 方法不同,UISafeInvoker 和 .NET Compact Framework Control.Invoke 与 BeginInvoke 方法一样,只支持 EventHandler 委托。因为 UISafeInvoker 仅支持一种委托类型,因此不需要使用对象数组来传递参数。相反,Invoke 与 BeginInvoke 接受直接传递给 EventHandler 委托中对应参数的对象和 EventArgs 参数。这里是每种方法的签名。
void Invoke(EventHandler eh, object obj, EventArgs eArgs);
IAsyncResult BeginInvoke(EventHandler eh, object obj, EventArgs eArgs);
使用 UISafeInvoker 就是简单地在 Form 类中声明一个引用,并在 Form 构造函数中创建一个实例。创建之后,UISafeInvoker 就内部跟踪创建它的线程,因此 Invoke 和 BeginInvoke 方法可以在同一个线程上运行期望的委托。作为 Form 构造函数的一部分而创建 UISafeInvoker,并且是在与所有窗体控件相同的线程上创建;因此,Invoke 或者 BeginInvoke 方法运行的任何委托都可以安全地更新 UI 控件。
这里是使用 UISafeInvoker 更新后的测试应用程序。
class MyForm : Form{
ListBox lbData ;
UISafeInvoker invoker ; // Declare UISafeInvoker
MyForm() {
InitializeComponent();
invoker = new UISafeInvoker(); // Create UISafeInvoker on main UI thread
Thread t = new Thread(new ThreadStart(Work1_));
t.Start() ; // Runs Work1_ on a background thread
}
void Work1_(){
// Wrap AddItem in delegate
EventHandler eh = new EventHandler(AddItem);
StreamReader rdr1 = new StreamReader(@"\My Documents\DataFile.dat");
string line = rdr1.ReadLine();
while(line != null) {
invoker.BeginInvoke(eh, line, EventArgs.Empty); // Pass to AddItem
line = rdr1.ReadLine();
}
}
// o receives the reference to line, e receives EventArgs.Empty
void AddItem(object o, EventArgs e)
{
string line = (string) o; // Upcast o
lbData.Items.Add(line); // Add to Listbox
}
}
对于 UISafeInvoker,我们的 .NET Compact Framework 应用程序已经克服了 .NET Compact Framework Control.Invoke 方法的局限性,现在可以提供简单、有效的线程内通信了。这种通信类似于 .NET Framework 中的通信,不需要额外的数据结构或者复杂的编码。
使用窗口消息
在内部,UISafeInvoker 非常简单,因为它仅完成两件事情:跟踪创建它的线程并提供一种在线程之间传输数据的可靠方法。
这种解决方案 — 虽然听起来有些陈旧 — 是基于窗口消息的。Windows 操作系统创建的所有窗口都有一个消息队列。应用程序可以通过使用 Microsoft Win32_ SDK 函数 SendMessage 和 PostMessage 将消息放置到该队列中。这些函数允许应用程序传递一个标识执行操作的整数标志和两个过去被称为 WParam 与 LParam 的消息定义数据值。
除了有一个主要的不同点,SendMessage 和 PostMessage 函数基本上是一样的。SendMessage 将消息放置到窗口消息队列中,并阻止消息直到窗口完成对它的处理。PostMessage 将消息放置到窗口消息队列中并立即返回。SendMessage 和 PostMessage 可以由任何线程调用,但是窗口总是在创建该窗口的线程上处理消息。这种行为隐含地解决了线程跟踪和在线程间传输数据的问题。
文章转自 http://www.diybl.com/course/3_program/jdkf/20071028/80824_2.html