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

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

 

许可协议:

CC BY-NC-SA

知识共享许可协议
《C#跨线程修改控件——从MSIL和汇编看Invoke, 多线程, 事件与事件委托》wenqiushi (cnblogs.com) 创作,采用 知识共享 署名-非商业性使用-相同方式共享 3.0 中国大陆 许可协议进行许可。请阅读原创性声明 。

 

正文:

相信大家刚开始写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 }
Director.cs

 

 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   }
Form1.cs

 

我们知道当多个线程同时竞争资源的访问权并尝试修改资源状态时,资源可能出现同步异常。因此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  }
更改过的Form1.cs

至此,问题已经彻底解决,本文也可以真正地结束了。。。

但是!!!我们都知道一个不想当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

private object MarshaledInvoke(Control caller, Delegate method, object[] args, bool synchronous)
    {
      if (!this.IsHandleCreated)
        throw new InvalidOperationException(System.Windows.Forms.SR.GetString("ErrorNoMarshalingThread"));
      if ((Control.ActiveXImpl) this.Properties.GetObject(Control.PropActiveXImpl) != null)
        System.Windows.Forms.IntSecurity.UnmanagedCode.Demand();
      bool flag = false;
      int lpdwProcessId;
      if (System.Windows.Forms.SafeNativeMethods.GetWindowThreadProcessId(new HandleRef((object) this, this.Handle), out lpdwProcessId) == System.Windows.Forms.SafeNativeMethods.GetCurrentThreadId() && synchronous)
        flag = true;
      ExecutionContext executionContext = (ExecutionContext) null;
      if (!flag)
        executionContext = ExecutionContext.Capture();
      Control.ThreadMethodEntry threadMethodEntry = new Control.ThreadMethodEntry(caller, this, method, args, synchronous, executionContext);
      lock (this)
      {
        if (this.threadCallbackList == null)
          this.threadCallbackList = new Queue();
      }
      lock (this.threadCallbackList)
      {
        if (Control.threadCallbackMessage == 0)
          Control.threadCallbackMessage = System.Windows.Forms.SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage");
        this.threadCallbackList.Enqueue((object) threadMethodEntry);
      }
      if (flag)
        this.InvokeMarshaledCallbacks();
      else
       //这里就是不添加任何防腐剂的纯天然原生态NativeMethod
      System.Windows.Forms.UnsafeNativeMethods.PostMessage(new HandleRef((object) this, this.Handle), Control.threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);
      if (!synchronous)
        return (object) threadMethodEntry;
      if (!threadMethodEntry.IsCompleted)
        this.WaitForWaitHandle(threadMethodEntry.AsyncWaitHandle);
      if (threadMethodEntry.exception != null)
        throw threadMethodEntry.exception;
      else
        return threadMethodEntry.retVal;
}
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()还是直接表白(无误)。那么这个属性是否真的如我们所想,仅仅是判断调用者线程呢?看代码:

[SRDescription("ControlInvokeRequiredDescr")]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Advanced)]
public bool InvokeRequired
{
      get
      {
        using (new Control.MultithreadSafeCallScope())
        {
          HandleRef hWnd;
          if (this.IsHandleCreated)
          {
            hWnd = new HandleRef((object) this, this.Handle);
          }
          else
          {
            Control marshalingControl = this.FindMarshalingControl();
            if (!marshalingControl.IsHandleCreated)
              return false;
            hWnd = new HandleRef((object) marshalingControl, marshalingControl.Handle);
          }
          int lpdwProcessId;
          return System.Windows.Forms.SafeNativeMethods.GetWindowThreadProcessId(hWnd, out lpdwProcessId) != System.Windows.Forms.SafeNativeMethods.GetCurrentThreadId();
        }
      }
    }
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,微软一匡天下之心昭然若揭。。。)

.method public hidebysig specialname instance void 
        add_OnReport(class DelegateDemo.PostEventHandler 'value') cil managed
{
  // 代码大小       23 (0x17)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldsfld     class DelegateDemo.PostEventHandler DelegateDemo.Director::_report
  IL_0006:  ldarg.1
  IL_0007:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
                                                                                          class [mscorlib]System.Delegate)
  IL_000c:  castclass  DelegateDemo.PostEventHandler
  IL_0011:  stsfld     class DelegateDemo.PostEventHandler DelegateDemo.Director::_report
  IL_0016:  ret
} // end of method Director::add_OnReport

.event DelegateDemo.PostEventHandler OnReport
{
  .addon instance void DelegateDemo.Director::add_OnReport(class DelegateDemo.PostEventHandler)
  .removeon instance void DelegateDemo.Director::remove_OnReport(class DelegateDemo.PostEventHandler)
} // end of event Director::OnReport
Director - MSIL

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

.method private hidebysig instance void  button1_Click(object sender,
                                                       class [mscorlib]System.EventArgs e) cil managed
{
  // 代码大小       66 (0x42)
  .maxstack  3
  .locals init ([0] class DelegateDemo.Director director,
           [1] class [mscorlib]System.Threading.Thread thread,
           [2] class [mscorlib]System.Threading.Thread '<>g__initLocal0')
  IL_0000:  nop
  IL_0001:  newobj     instance void DelegateDemo.Director::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  ldarg.0
  IL_0009:  ldftn      instance void DelegateDemo.Form1::director_OnReport(string)
  IL_000f:  newobj     instance void DelegateDemo.PostEventHandler::.ctor(object,
                                                                          native int)
  IL_0014:  callvirt   instance void DelegateDemo.Director::add_OnReport(class DelegateDemo.PostEventHandler)
  IL_0019:  nop
  IL_001a:  ldnull
  IL_001b:  ldftn      void DelegateDemo.Director::Test()
  IL_0021:  newobj     instance void [mscorlib]System.Threading.ThreadStart::.ctor(object,
                                                                                   native int)
  IL_0026:  newobj     instance void [mscorlib]System.Threading.Thread::.ctor(class [mscorlib]System.Threading.ThreadStart)
  IL_002b:  stloc.2
  IL_002c:  ldloc.2
  IL_002d:  ldstr      "thdDirector"
  IL_0032:  callvirt   instance void [mscorlib]System.Threading.Thread::set_Name(string)
  IL_0037:  nop
  IL_0038:  ldloc.2
  IL_0039:  stloc.1
  IL_003a:  ldloc.1
  IL_003b:  callvirt   instance void [mscorlib]System.Threading.Thread::Start()
  IL_0040:  nop
  IL_0041:  ret
} // end of method Form1::button1_Click
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处:

--- Director.cs ----------------

00000000  push        ebp  //各种压栈,为后面还原
00000001  mov         ebp,esp 
00000003  push        edi 
00000004  push        esi 
00000005  push        ebx 
00000006  sub         esp,38h 
00000009  xor         eax,eax 
0000000b  mov         dword ptr [ebp-10h],eax 
0000000e  xor         eax,eax 
00000010  mov         dword ptr [ebp-1Ch],eax 
00000013  mov         dword ptr [ebp-3Ch],ecx 
00000016  mov         dword ptr [ebp-40h],edx 
00000019  cmp         dword ptr ds:[00289080h],0 
00000020  je          00000027 
00000022  call        78C0FD41 
//这里开始对应 add { _report += value; }
00000027  nop  //获得数据段地址寄存器偏移量02A184B8h(每次运行不同)处的值,赋给ecx寄存器,这个偏移量下面还会见到。
00000028  mov         ecx,dword ptr ds:[02A184B8h] 
0000002e  mov         edx,dword ptr [ebp-40h] 
00000031  call        77EE1804  //调用Delegate Combine()
00000036  mov         dword ptr [ebp-44h],eax 
00000039  cmp         dword ptr [ebp-44h],0 
0000003d  je          0000005E 
0000003f  mov         eax,dword ptr [ebp-44h] 
00000042  cmp         dword ptr [eax],4430824h 
00000048  jne         0000004F 
0000004a  mov         eax,dword ptr [ebp-44h] 
0000004d  jmp         0000005C  //直接跳到00000061
0000004f  mov         edx,dword ptr [ebp-44h] 
00000052  mov         ecx,4430824h 
00000057  call        7899A73E 
0000005c  jmp         00000061 
0000005e  mov         eax,dword ptr [ebp-44h] 
00000061  lea         edx,ds:[02A184B8h] 
00000067  call        789911C8  //未跟踪
0000006c  nop 
0000006d  lea         esp,[ebp-0Ch] 
00000070  pop         ebx 
00000071  pop         esi 
00000072  pop         edi 
00000073  pop         ebp 
00000074  ret  
Director.cs - Asm

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

        public static Delegate Combine(Delegate a, Delegate b)
        {
            if (a == null)
            {
                return b;
            }
            return a.CombineImpl(b);
        }
       
Combine

 来到了断点1:

--- Director.cs ----------------
//对应_report(counter.ToString(CultureInfo.InvariantCulture));
00000038  nop  //这个非常眼熟的偏移地址02A184B8h值又送给eax寄存器,这个偏移就是数据段中函数地址
00000039  mov         eax,dword ptr ds:[02A184B8h] 
0000003e  mov         dword ptr [ebp-48h],eax 
00000041  lea         eax,[ebp-3Ch] 
00000044  mov         dword ptr [ebp-4Ch],eax 
00000047  call        77E72110  //这里构造了个CultureInfo
0000004c  mov         dword ptr [ebp-50h],eax 
0000004f  mov         edx,dword ptr [ebp-50h] 
00000052  mov         ecx,dword ptr [ebp-4Ch] 
00000055  call        7838EDA4  //调用NumberFormatInfo(), 还是Culture相关的
0000005a  mov         dword ptr [ebp-54h],eax 
0000005d  mov         edx,dword ptr [ebp-54h] 
00000060  mov         ecx,dword ptr [ebp-48h] 
00000063  mov         eax,dword ptr [ecx+0Ch] 
00000066  mov         ecx,dword ptr [ecx+4] 
00000069  call        eax  //此时eax中的值就是ds:[02A184B8h]的值,call后直接来到director_OnReport(),见下。
0000006b  nop
Director.cs - Asm

直接跳转到了函数director_OnReport()

//直接跳到了函数director_OnReport()
00000052  nop
            //int value = Convert.ToInt32(postStatus);
00000053  mov         ecx,dword ptr [ebp-40h] 
00000056  call        03B4E948 
0000005b  mov         dword ptr [ebp-58h],eax 
0000005e  mov         eax,dword ptr [ebp-58h] 
00000061  mov         dword ptr [ebp-44h],eax 
            //if (this.progressBar1.InvokeRequired)
00000064  mov         eax,dword ptr [ebp-3Ch] 
00000067  mov         ecx,dword ptr [eax+00000144h] 
0000006d  mov         eax,dword ptr [ecx] 
0000006f  call        dword ptr [eax+00000128h] 
00000075  mov         dword ptr [ebp-5Ch],eax 
00000078  cmp         dword ptr [ebp-5Ch],0 
0000007c  sete        al 
0000007f  movzx       eax,al 
00000082  mov         dword ptr [ebp-50h],eax 
00000085  cmp         dword ptr [ebp-50h],0 
00000089  jne         0000012D 
0000008f  nop 
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:这是本人的处女博(无误),以后要养成写博客的好习惯~

 

- END -

posted on 2013-05-28 11:43  wings27  阅读(1575)  评论(3编辑  收藏  举报

导航