ⅠWindows 窗体控件的线程安全性和InvokeRequired属性
Windows 窗体中的控件被绑定到特定的线程,不具备线程安全性。这就是说当我们企图从一个线程中操作在另一个线程中创建的控件时,可能会产生意想不到的错误。为此多线程环境下操作窗体控件时必须注意要使用那些线程安全的方法、成员或事件。
多数控件从Windows基类(比如Control类)那里继承并公开了InvokeRequired 属性。当创建控件句柄的线程和调用线程不同时,该属性指示调用方在调用控件的方法时是否必须调用 Invoke 方法。除InvokeRequired 属性以外,控件上还有以下四个线程安全的方法可供调用:Invoke、BeginInvoke、EndInvoke 和 CreateGraphics。当从另一个线程对除此之外的所有其他成员的进行调用时,应使用这些 Invoke 方法中的一个。
虽然InvokeRequired 属性是线程安全的,但是在使用它的时候仍然需要小心。InvokeRequired 属性为False时,有可能它意味着我们可以不使用Invoke(调用发生在同一线程上),但那只是可能。我们需要知道,对于句柄尚未建立的控件, InvokeRequired 会沿控件的父级链搜索,直到它找到有窗口句柄的控件或窗体为止。如果找不到合适的句柄,InvokeRequired 方法将返回 false。 这时候调用控件上属性、方法或事件就很可能导致在后台线程上创建控件的句柄,从而隔离不带消息泵的线程上的控件并使应用程序不稳定。
通常来说,这种问题仅会发生在下面所说的情况中。那就是我们在应用程序主窗体的构造函数中创建了后台线程(如同在 Application.Run(new MainForm()) 中),并试图在窗体已经显示或取消 Application.Run 之前启动该线程。 因为我们容易忽略一个事实,那就是在启动窗体的Load 事件之前,除非存在强制操作,否则窗体和窗体控件的句柄是不会被创建的。
为了避免上述问题,当 InvokeRequired 在后台线程上返回 false 时,我们往往需要检查 IsHandleCreated 的值来确定控件的句柄是否被创建。如果尚未创建,那么我们必须等待窗体句柄建立(可以通过调用 Handle 属性强制创建句柄、或者等待窗体的 Load 事件)后去才启动后台进程。 更优的选择是使用 SynchronizationContext 返回的 SynchronizationContext,而不是使用控件进行线程间封送处理。
Ⅱ示例:MultiRow多线程加载数据
首先,我们为多线程操作MultiRow提供一个逻辑控制和线程管理的类,本例尽量简单以突出显示我们是怎样使用InvokeRequired 属性和Invoke方法的。程序中我们使用了MultiRow父窗体的Invoke方法而不是MultiRow自己的Invoke方法。
/**//// <summary>
/// 简单的数据加载控制
/// </summary>
public class DataLoadManager
...{
/**//// <summary>
/// 定义结构体,用于保存数据开始和结束行
/// </summary>
struct dataIndexForThread
...{
public int StartRow ;
public int EndRow;
}
//创建两个线程
Thread t1;
Thread t2;
//开始和结束控制
dataIndexForThread rowsInThread1;
dataIndexForThread rowsInThread2;
//记录已经完成的线程数
int completedThread = 0;
//控件和控件父窗体
GrapeCity.Win.ElTabelle.MultiRowSheet _multirow;
Form _Parent;
//提供事件以加载数据
public delegate void loadData(int rowIndex);
public event loadData LoadDataToMultiRow;
//提供事件以在数据加载前后作客户处理
public delegate void loadOver();
public event loadOver LoadDataIsCompleted;
public event loadOver LoadDataIsBegin;
public DataLoadManager(int MaxRows)
...{
rowsInThread1.StartRow = 0;
rowsInThread1.EndRow = MaxRows / 2;
rowsInThread2.StartRow = rowsInThread1.EndRow;
rowsInThread2.EndRow = MaxRows;
}
/**//// <summary>
/// 为加载指定行范围内的数据提供逻辑控制
/// </summary>
/// <param name="rows">该进程内处理的行范围</param>
private void loadDataInSeparatedThread(object rows)
...{
dataIndexForThread rowsInThread = (dataIndexForThread)rows;
if (_multirow.InvokeRequired)
...{
_Parent.Invoke(new WaitCallback(loadDataInSeparatedThread), new object[] ...{ rows });
}
else
...{
for (int i = rowsInThread.StartRow; i < rowsInThread.EndRow; i++)
...{
if (LoadDataToMultiRow != null)
LoadDataToMultiRow(i);
}
completedThread += 1;
if (completedThread == 2 && LoadDataIsCompleted != null)
...{
LoadDataIsCompleted();
}
}
}
/**//// <summary>
/// 启动多线程
/// </summary>
/// <param name="Parent"></param>
/// <param name="multirow"></param>
public void Start(Form Parent, GrapeCity.Win.ElTabelle.MultiRowSheet multirow)
...{
_multirow = multirow;
_Parent = Parent;
if (LoadDataIsBegin != null) LoadDataIsBegin();
t1 = new Thread(new ParameterizedThreadStart(loadDataInSeparatedThread));
t2 = new Thread(new ParameterizedThreadStart(loadDataInSeparatedThread));
t1.IsBackground = true;
t1.Start(rowsInThread1);
t2.IsBackground = true;
t2.Start(rowsInThread2);
}
}
以下代码用来验证多线程MultiRow加载数据。该代码架设你拥有一个WinForm窗口,该窗口上添加有一个MultiRow控件和一个Button控件。该程序需要引用System.Threading命名空间。
//在Button的Click事件内添加以下代码
DataLoadManager dataManager = new DataLoadManager(MaxRows);
dataManager.LoadDataIsBegin += new DataLoadManager.loadOver(setDataToMultiRowBegin);
dataManager.LoadDataToMultiRow += new DataLoadManager.loadData(setDataToMultiRow);
dataManager.LoadDataIsCompleted += new DataLoadManager.loadOver(setDataToMultiRowCompleted);
dataManager.Start(this, multiRowSheet0);
//在MultiRow的窗口内添加以下方法
private void setDataToMultiRowBegin(
{
multiRowSheet0.BeginUpdate();
multiRowSheet0.MaxMRows = 0;
multiRowSheet0.MaxMRows = MaxRows;
}
private void setDataToMultiRowCompleted()
{
multiRowSheet0.EndUpdate();
}
private void setDataToMultiRow(int row)
{
multiRowSheet0[row, "列名"].Text = "该行该列的取值";
}
Ⅲ总结
在本示例中,我们发现多线程加载数据比通常的加载方式耗费了更多的时间,而应用多线程的目的却往往正是为了节省时间。求其原因,是本例应用的场合不对。多线程适用于耗时计算等场合。比如说本例中在数据加载的同时需要大量计算的话,那么为了增强用户体验使其不至长时间等待,那么可以考虑使用多线程。很多时候甚至可以先加载显示那些容易得到的数据。而在后台用多线程并行计算那些耗时数据然后「默默的」填充到MultiRow上。
更重要的是本例存在严重的安全隐患,它无法应对我们第一节中提出的那些问题。比如说,我们想向窗口动态添加一个MultiRow,它加载完数据之后添加显示到窗体上。那么如果在主线程调用Controls.Add()方法将控件加载到窗口之前,我们无意间在后台进程中创建了控件句柄,就将可能产生以下几种错误:创建句柄的进程完成工作而结束,其他后台进程将无法继续为MultiRow加载数据,系统提示先行建立的线程不存在;进程顺利完成数据加载,但是主进程执行Controls.Add()方法报错,程序因此无法将该MultiRow添加到界面。而且这种错误往往很细微难于发现。至于具体的测试代码这里就不在赘述。解决的手段也第一节中已经交待,具体实现因需求而异。但是一般来说存在一个基本准则,除了线程独自使用的控件,不要在后台线程中创建空间句柄,全局控件句柄的持有者必须是伴随程序进程始终的线程。