相信大家刚开始写winform的时候都遇到过这样的问题,当跨线程修改控件属性时会遇到如下的异常:
线程间操作无效: 从不是创建控件"progressBar1"的线程访问它。
这是相应的产生上述异常的代码:
1 #region Auto-Generated Properties 2 3 // DelegateDemo - Director.cs 4 // by Wings 5 // Last Modified : 2013-05-28 11:43 6 7 #endregion 8 9 #region Using Block 10 11 using System.Globalization; 12 using System.Threading; 13 14 #endregion 15 16 namespace DelegateDemo 17 { 18 public delegate void PostEventHandler(string postStatus); 19 20 internal class Director 21 { 22 private static PostEventHandler _report; 23 24 public event PostEventHandler OnReport 25 { 26 add { _report += value; } 27 remove { _report -= value; } 28 } 29 30 public static void Test() 31 { 32 int counter = 0; 33 while (counter++ < 100) 34 { 35 _report(counter.ToString(CultureInfo.InvariantCulture)); 36 Thread.Sleep(100); 37 } 38 } 39 } 40 }
1 #region Auto-Generated Properties 2 3 // DelegateDemo - Form1.cs 4 // by Wings 5 // Last Modified : 2013-05-27 19:54 6 7 #endregion 8 9 #region Using Block 10 11 using System; 12 using System.Threading; 13 using System.Windows.Forms; 14 15 #endregion 16 17 namespace DelegateDemo 18 { 19 public partial class Form1 : Form 20 { 21 public Form1() 22 { 23 InitializeComponent(); 24 } 25 26 private void button1_Click(object sender, EventArgs e) 27 { 28 Director director = new Director(); 29 director.OnReport += director_OnReport; 30 Thread thread = new Thread(Director.Test) 31 { 32 Name = "thdDirector" 33 }; 34 thread.Start(); 35 } 36 37 private void director_OnReport(string postStatus) 38 { 39 int value = Convert.ToInt32(postStatus); 40 this.progressBar1.Value = value; //此处产生异常 41 } 42 } 43 }
我们知道当多个线程同时竞争资源的访问权并尝试修改资源状态时,资源可能出现同步异常。因此CLR才会禁止这种跨线程修改主窗体控件的行为。
一个简单粗暴(但十分有效)的方法是在主窗体构造函数中加入CheckForIllegalCrossThreadCalls = false;
像这样:
public Form1() { InitializeComponent(); CheckForIllegalCrossThreadCalls = false; }
附上msdn的解释:
获取或设置一个值,该值指示是否捕获对错误线程的调用,这些调用在调试应用程序时访问控件的 Handle 属性。
因此设为false后将不再检查非法跨线程调用。问题解决,本文也可以到此结束了(大误啊、、、)
毕竟跨线程调用是不安全的,可能导致同步失败。所以我们采用正统一点的方法来解决,那就是调用control的Invoke()或BeginInvoke()方法。
二者的差别在于BeginInvoke()是异步的,这里为了防止Director.Test()执行时主窗体关闭导致句柄失效进而产生异常,我们使用BeginInvoke()方法进行异步调用。
1 #region Auto-Generated Properties 2 3 // DelegateDemo - Form1.cs 4 // by Wings 5 // Last Modified : 2013-05-28 13:06 6 7 #endregion 8 9 #region Using Block 10 11 using System; 12 using System.Threading; 13 using System.Windows.Forms; 14 15 #endregion 16 17 namespace DelegateDemo 18 { 19 public partial class Form1 : Form 20 { 21 public Form1() 22 { 23 InitializeComponent(); 24 } 25 26 private void button1_Click(object sender, EventArgs e) 27 { 28 Director director = new Director(); 29 director.OnReport += director_OnReport; 30 Thread thread = new Thread(Director.Test) 31 { 32 Name = "thdDirector" 33 }; 34 thread.Start(); 35 } 36 37 private void director_OnReport(string postStatus) 38 { 39 int value = Convert.ToInt32(postStatus); 40 if (this.progressBar1.InvokeRequired) 41 { 42 SetValueCallback setValueCallback = delegate(int i) 43 { 44 this.progressBar1.Value = i; 45 }; 46 this.progressBar1.BeginInvoke(setValueCallback, value); 47 } 48 else 49 { 50 this.progressBar1.Value = value; 51 } 52 } 53 54 private delegate void SetValueCallback(int value); 55 } 56 }
至此,问题已经彻底解决,本文也可以真正地结束了。。。
但是!!!我们都知道一个不想当Geek的码农不是好程序猿~
于是乎我们应再次发扬Geek精神,剥去.NET粉饰的外衣,窥其真理的内核。
先从Invoke()入手,看到其源码:
public object Invoke(Delegate method, params object[] args) { using (new Control.MultithreadSafeCallScope()) return this.FindMarshalingControl().MarshaledInvoke(this, method, args, true); }
而BeginInvoke()差别仅仅在于MarshaledInvoke()的参数synchronous:
public IAsyncResult BeginInvoke(Delegate method, params object[] args) { using (new Control.MultithreadSafeCallScope()) return (IAsyncResult) this.FindMarshalingControl().MarshaledInvoke(this, method, args, false); }
实质都是调用了MarshaledInvoke方法。Marshaled这个词常写NativeMethods的同学一定很熟悉。中文翻译我还真不知道,这里给出维基百科的释义作为参考:
In computer science, marshalling (sometimes spelled marshaling with a single l) is the process of transforming the memory representation of an object to a data format suitable for storage or transmission, and it is typically used when data must be moved between different parts of a computer program or from one program to another. Marshalling is similar to serialization and is used to communicate to remote objects with an object, in this case a serialized object. It simplifies complex communication, using custom/complex objects to communicate instead of primitives. The opposite, or reverse, of marshalling is called unmarshalling (or demarshalling, similar to deserialization).
所以.NET的“暗箱操作”很有可能就在MarshaledInvoke里面。我们点进去看一下,当然主要关注NativeMethods
果然被我们找到了,这个System.Windows.Forms.UnsafeNativeMethods.PostMessage()就是WinAPI封装过后的NativeMethod了。当然它披上另一件衣服之后也是MFC里面的CWnd::PostMessage, 负责向窗体消息队列中放置一条消息,并且不等待消息被处理而直接返回(即异步,这也是与SendMessage的差别)。(Places a message in the window's message queue and then returns without waiting for the corresponding window to process the message.)
这也就解释了上述情况发生的原因,调用Invoke()而不是直接更改控件值使得主窗体能够将消息加入自身的消息队列中,从而在合适的时间处理消息,这样跨线程更改控件值就转变为窗体线程自己更改控件值,也就是从创建控件的线程(窗体主线程)访问控件,避免了之前的错误:“从不是创建控件"progressBar1"的线程访问它。”
不过还有一个问题,如果本来就是窗体线程对控件进行访问呢,毫无疑问直接设置值即可。在上面的代码中我使用InvokeRequired属性来判断控件更改者是否来自于其他线程,从而决定是调Invoke()还是直接表白(无误)。那么这个属性是否真的如我们所想,仅仅是判断调用者线程呢?看代码:
还真是这么直白,最后的return写的非常清楚。
至此,我们已经理解了Invoke的具体实现。下面来看事件委托,为什么Director.Test()能够触发Form1.cs的director_OnReport()回调函数。
我们在Form1.cs中的button1_Click()函数中添加了回调director.OnReport += director_OnReport;于是Director类OnReport事件执行了add{_report += value;}完成添加回调绑定过程。基于上面的现象我们知道progressBar1是在非窗体线程被更改的(见Invoke实现),既然是来自非窗体线程的更改,那么会不会是本来在窗体类中的director_OnReport(string postStatus)函数在回调绑定完成之后直接被替换到了Director.Test()中的_report(counter.ToString(CultureInfo.InvariantCulture));呢?
既然我们从表象上有理由怀疑这一点,那么就应当实际验证一下。只可惜C#封装的事件委托使得我们从.NET的源码中也难以知晓其底层实现。正所谓“不识庐山真面目,只缘身在此山中。”
为了理解其底层实现,我们必须先走出C#语言层面这座山。那就先看看Director类的MSIL吧(话说MSIL现已被微软正名为CIL,微软一匡天下之心昭然若揭。。。)
OnReport事件的内容被编译为两个函数。我们先只看add_OnReport这个函数,无非是与Property的Getter和Setter类似,对内绑定到_report()函数。那么再来看Form1中对OnReport事件的注册:
其中IL_0009: ldftn位置到IL_000f: newobj位置声明并实例化了director_OnReport作为委托的target,而IL_0014: callvirt位置调用了add_OnReport()进行实际意义上的绑定。
然后从IL_001b: ldftn位置开始实例化新线程并进行相关赋值操作,直到IL_003b: callvirt位置调用Thead::Start()运行线程。
这样我们已经基本理清了绑定的实现过程,但是代码在执行时是否如我上面所说是“函数在回调绑定完成之后直接被替换”这样呢?想要验证就必须再看MSIL的底层实现,那是什么呢?对了,就是汇编。(//=_=老是自问自答有意思么。。。)
打开高端大气上档次的反汇编界面,在Director类中设定断点:
断点0:行26:add { _report += value; }
断点1:行35:_report(counter.ToString(CultureInfo.InvariantCulture));
开始调试,点击button1,第一次中断在断点0处:
其中Combine代码(无所谓了):
来到了断点1:
直接跳转到了函数director_OnReport()
这就充分说明在C#代码层面上执行的_report()函数和director_OnReport()回调函数本质上是同一个函数(段地址相同),也恰好解释了为什么Form1类中的private函数为什么可以在另一个类中触发。因为C#也好,CIL也好,都是表层的封装。而在CLR虚拟机中实实在在运行的,是CLR Assembly. 我们说CLR是虚拟机,这个“虚拟”仅仅指CLR中的指令并非与物理硬件相关联,但是CLR以及其中的指令都是真实存在的,与真实机上的x86 CPU指令本质上是相同的。C#美轮美奂的亭台楼榭都建立在Assembly的一砖一瓦之上。而在CLR Assembly层面,只有内核级的概念,有内存管理,有线程调度。。。但是没有类级属性,没有成员函数,没有作用域可访问性控制。我们在使用C#封装好的模块和功能模型时,如果能够同时理解其底层实现,相信会对软件开发工作大有裨益。
忽然发现写了这么多。。而且好像逻辑很混乱的样子。。权当给小白入门看的吧~ 也欢迎各路大神不吝赐教。 另PS:这是本人的处女博(无误),以后要养成写博客的好习惯~