C#跨线程修改控件——从MSIL和汇编看Invoke, 多线程, 事件与事件委托

摘要:
相信大家刚开始写winform的时候都遇到过这样的问题,当跨线程修改控件属性时会遇到如下的异常:线程间操作无效:从不是创建控件"progressBar1"的线程访问它。因此CLR才会禁止这种跨线程修改主窗体控件的行为。一个简单粗暴的方法是在主窗体构造函数中加入CheckForIllegalCrossThreadCalls=false;像这样:publicForm1(){InitializeComponent();CheckForIllegalCrossThreadCalls=false;}附上msdn的解释:获取或设置一个值,该值指示是否捕获对错误线程的调用,这些调用在调试应用程序时访问控件的Handle属性。因此设为false后将不再检查非法跨线程调用。

相信大家刚开始写winform的时候都遇到过这样的问题,当跨线程修改控件属性时会遇到如下的异常:

线程间操作无效: 从不是创建控件"progressBar1"的线程访问它。

这是相应的产生上述异常的代码:

C#跨线程修改控件——从MSIL和汇编看Invoke, 多线程, 事件与事件委托第1张
复制代码
 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 }
复制代码
C#跨线程修改控件——从MSIL和汇编看Invoke, 多线程, 事件与事件委托第1张
复制代码
 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()方法进行异步调用。

C#跨线程修改控件——从MSIL和汇编看Invoke, 多线程, 事件与事件委托第1张
复制代码
 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

C#跨线程修改控件——从MSIL和汇编看Invoke, 多线程, 事件与事件委托第10张MarshaledInvoke

果然被我们找到了,这个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()还是直接表白(无误)。那么这个属性是否真的如我们所想,仅仅是判断调用者线程呢?看代码:

C#跨线程修改控件——从MSIL和汇编看Invoke, 多线程, 事件与事件委托第11张InvokeRequired

还真是这么直白,最后的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,微软一匡天下之心昭然若揭。。。)

C#跨线程修改控件——从MSIL和汇编看Invoke, 多线程, 事件与事件委托第12张Director - MSIL

OnReport事件的内容被编译为两个函数。我们先只看add_OnReport这个函数,无非是与Property的Getter和Setter类似,对内绑定到_report()函数。那么再来看Form1中对OnReport事件的注册:

C#跨线程修改控件——从MSIL和汇编看Invoke, 多线程, 事件与事件委托第13张button1_Click - MSIL

其中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处:

C#跨线程修改控件——从MSIL和汇编看Invoke, 多线程, 事件与事件委托第14张Director.cs - Asm

其中Combine代码(无所谓了):

C#跨线程修改控件——从MSIL和汇编看Invoke, 多线程, 事件与事件委托第15张Combine

来到了断点1:

C#跨线程修改控件——从MSIL和汇编看Invoke, 多线程, 事件与事件委托第16张Director.cs - Asm

直接跳转到了函数director_OnReport()

C#跨线程修改控件——从MSIL和汇编看Invoke, 多线程, 事件与事件委托第17张Form1.cs - Asm

这就充分说明在C#代码层面上执行的_report()函数和director_OnReport()回调函数本质上是同一个函数(段地址相同),也恰好解释了为什么Form1类中的private函数为什么可以在另一个类中触发。因为C#也好,CIL也好,都是表层的封装。而在CLR虚拟机中实实在在运行的,是CLR Assembly. 我们说CLR是虚拟机,这个“虚拟”仅仅指CLR中的指令并非与物理硬件相关联,但是CLR以及其中的指令都是真实存在的,与真实机上的x86 CPU指令本质上是相同的。C#美轮美奂的亭台楼榭都建立在Assembly的一砖一瓦之上。而在CLR Assembly层面,只有内核级的概念,有内存管理,有线程调度。。。但是没有类级属性,没有成员函数,没有作用域可访问性控制。我们在使用C#封装好的模块和功能模型时,如果能够同时理解其底层实现,相信会对软件开发工作大有裨益。

忽然发现写了这么多。。而且好像逻辑很混乱的样子。。权当给小白入门看的吧~ 也欢迎各路大神不吝赐教。 另PS:这是本人的处女博(无误),以后要养成写博客的好习惯~

免责声明:文章转载自《C#跨线程修改控件——从MSIL和汇编看Invoke, 多线程, 事件与事件委托》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇[LUOGU] P1828 香甜的黄油 Sweet ButterRevit 二次开发 元素创建与修改练习下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

php的session

来源:http://blog.163.com/lgh_2002/blog/static/4401752620105246517509/ http协议是WEB服务器与客户 端(浏览器)相互通信的协议,它是一种无状态协议。所谓无状态,指的是不会维护http请求数据,http请求是独立的,非持久的。而越来越复杂的WEB 应用,需要保存一些用户状态信息。这时候,S...

如何理解 Java 多线程

进程和线程的概念是操作系统的概念,因此你可能需要看看大学有关《操作系统原理》这本书中的内容才能理解什么是进程和线程。 简单来说进程和线程涉及到 CPU 的使用和内存的分配。 可以想象下你的浏览器,如果你打开了一个浏览器,表示你启动了一个进程,如果你再打开多个标签页,表示你启动了不同的线程(敲黑板:浏览器启动不同的标签页,其实启动的是不同的进程,不是启动线程...

委托和多线程(一)

         定义:委托是一个类型安全的对象,它指向程序中另一个以后会被调用的方法(或多个方法)。通俗的说,委托是一个可以引用方法的对象,当创建一个委托,也就创建一个引用方法的对象,进而就可以调用那个方法,即委托可以调用它所指的方法。 委托的试用步骤: 1、定义委托:权限修饰符   delegate   返回值     委托名 (参数); 2、声明委托...

libevent编程疑难解答

http://blog.csdn.net/luotuo44/article/details/39547391 转载请注明出处:http://blog.csdn.net/luotuo44/article/details/39547391 正常情况下应该在libevent的回调中调用event_add函数, 如果想下其他的线程中调用event_add,是线程不...

生产者和消费者模型

生产者和消费者模型 线程通信:不同的线程执行不同的任务,如果这些任务有某种关系,各个线程必须要能够通信,从而完成工作。线程通信中的经典问题:生产者和消费者问题 模型: 这个模型也体现了面向对象的设计理念:低耦合 也就是为什么生产者生产的东西为什么不直接给消费者,还有经过一个缓冲区(共享资源区) 这就相当于去包子店吃包子,你要5个包子,老板把5个人包子放在...

C# 线程thread

一、问题总结  1. 在WinForm开发过程中用到线程时,往往需要在线程中访问线程外的控件,比如:设置textbox的Text值等等。如果直接访问UI控件会报出“从不是创建控件的线程访问它”错误。控件是在主线程中创建的(比如this.Controls.Add(...);),在其它线程直接访问主线程控件,与主线程发生线程冲突。 解决方法: 在控件响应函数中...