我的多线程WinForm程序老是抛出InvalidOperationException ,怎么解决?
在WinForm中使用多线程时,常常遇到一个问题,当在子线程(非UI线程)中修改一个控件的值:比如修改进度条进度,时会抛出如下错误
Cross-thread operation not valid: Control ’XXX’ accessed from a thread other than the thread it was created on.
在VS2005或者更高版本中,只要不是在控件的创建线程(一般就是指UI主线程)*问控件的属性就会抛出这个错误,解决方法就是利用控件提供的 Invoke和BeginInvoke把调用封送回UI线程,也就是让控件属性修改在UI线程上执行,下面列出会报错的代码和他的修改版本
ThreadStart threadStart=new ThreadStart(Calculate);//通过ThreadStart委托告诉子线程讲执行什么方法
Thread thread=new Thread(threadStart);
thread.Start();
public void Calculate(){
double Diameter=0.5;
double result=Diameter*Math.PI;
CalcFinished(result);//计算完成需要在一个文本框里显示
}
public void CalcFinished(double result){
this.TextBox1.Text=result.ToString();//会抛出错误
}
Thread thread=new Thread(threadStart);
thread.Start();
public void Calculate(){
double Diameter=0.5;
double result=Diameter*Math.PI;
CalcFinished(result);//计算完成需要在一个文本框里显示
}
public void CalcFinished(double result){
this.TextBox1.Text=result.ToString();//会抛出错误
}
上面加粗的地方在debug的时候会报错,最直接的修改方法是修改Calculate这个方法如下
delegate void changeText(double result);
public void Calculate(){
double Diameter=0.5;
double result=Diameter*Math.PI;
this.BeginInvoke(new changeText(CalcFinished),t.Result);//计算完成需要在一个文本框里显示
}
public void Calculate(){
double Diameter=0.5;
double result=Diameter*Math.PI;
this.BeginInvoke(new changeText(CalcFinished),t.Result);//计算完成需要在一个文本框里显示
}
这样就ok了,但是最漂亮的方法是不去修改Calculate,而去修改CalcFinished这个方法,因为程序里调用这个方法的地方可能很多,由于加了是否需要封送的判断,这样修改还能提高非跨线程调用时的性能
delegate void changeText(double result);
public void CalcFinished(double result){
if(this.InvokeRequired){
this.BeginInvoke(new changeText(CalcFinished),t.Result);
}
else{
this.TextBox1.Text=result.ToString();
}
}
public void CalcFinished(double result){
if(this.InvokeRequired){
this.BeginInvoke(new changeText(CalcFinished),t.Result);
}
else{
this.TextBox1.Text=result.ToString();
}
}
上面的做法用到了Control的一个属性InvokeRequired(这个属性是可以在其他线程里访问的),这个属性表明调用是否来自另非UI线程,如果是,则使用BeginInvoke来调用这个函数,否则就直接调用,省去线程封送的过程
Invoke,BeginInvoke干什么用的,内部是怎么实现的?
这两个方法主要是让给出的方法在控件创建的线程上执行
Invoke使用了Win32API的SendMessage,
UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);
BeginInvoke使用了Win32API的PostMessage
UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);
这两个方法向UI线程的消息队列中放入一个消息,当UI线程处理这个消息时,就会在自己的上下文中执行传入的方法,换句话说凡是使用BeginInvoke和Invoke调用的线程都是在UI主线程中执行的,所以如果这些方法里涉及一些静态变量,不用考虑加锁的问题
每个线程都有消息队列吗?
不是,只有创建了窗体对象的线程才会有消息队列(下面给出<Windows 核心编程>关于这一段的描述)
当一个线程第一次被建立时,系统假定线程不会被用于任何与用户相关的任务。这样可以减少线程对系统资源的要求。但是,一旦这个线程调用一个与图形用户界 面有关的函数(例如检查它的消息队列或建立一个窗口),系统就会为该线程分配一些另外的资源,以便它能够执行与用户界面有关的任务。特别是,系统分配一个 threadinfo结构,并将这个数据结构与线程联系起来。
这个threadinfo结构包含一组成员变量,利用这组成员,线程可 以认为它是在自己独占的环境中运行。threadinfo是一个内部的、未公开的数据结构,用来指定线程的登记消息队列(posted- message queue)、发送消息队列( send-message queue)、应答消息队列( reply - message queue)、虚拟输入队列(virtualized-input queue)、唤醒标志(wake flag)、以及用来描述线程局 部输入状态的若干变量。下图描述了threadinfo结构和与之相联系的三个线程。
为什么Winform不允许跨线程修改UI线程控件的值
在vs2003下,使用子线程调用ui线程创建的控件的属性是不会有问题的,但是编译的时候会出现警告,但是vs2005及以上版本就会有这样的问题,下面是msdn上的描述
"当您在 Visual Studio 调试器中运行代码时,如果您从一个线程访问某个 UI 元素,而该线程不是创建该 UI 元素时所在的线程,则会 引发 InvalidOperationException。调试器引发该异常以警告您存在危险的编程操作。UI 元素不是线程安全的,所以只应在创建它 们的线程上进行访问"
从上面可以看出,这个异常实际是debugger耍的花招,也就是说,如果你直接运行程序的exe文件,或者利用运行而不调试(Ctrl+F5)来运行你 的程序,是不会抛出这样的异常的.大概ms发现v2003的警告对广大开发者不起作用,所以用了一个比较狠一点的方法。
不过问题依然存在:既然这样设计的原因主要是因为控件的值非线程安全,那么DotNet framework中非线程安全的类千千万万,为什么偏偏跨线程修改Control的属性会有这样严格的限制策略呢?
这个问题我还回答不好,希望博友们能够予以补充