当你运行这个程序时,当耗时操作结束后,啪嚓一下,程序出异常了:
Control.Invoke和Control.BeginInvoke就是“发短信”的方法,如果使用Control.Invoke发短信,那么甲线程就会像个痴情的汉子,一直等待着乙线程的回音,而如果使用Control.BeginInvoke发送短信,那发完短信后,甲线程就会忙活自己的,等乙线程处理完再来瞧瞧。
注意:有人看到了BeginInvoke方法来了个Begin,心里可能在想,这是异步的特征啊,那是不是像上篇文章中使用delegate的BeginInvoke方法那样,启动一个worker thread?记住,这里的BeginInvoke是异步操作,但不是通过线程来实现的,具体方式后面有介绍。
我们先来看看如何使用Control.Invoke和Control.BeginInvoke(本文为了区分Control.BeginInvoke与delegate.BeginInvoke的区别,一直带上Control前缀)来发短信:
1: /// <summary>
2: /// 假设这是一个查询数据的方法,会很耗时
3: /// </summary>
4: /// <returns>返回从数据库查询结果</returns>
5: private string QueryDataBase()
6: {
7: Thread.Sleep(10 * 1000);
8: return "yuyijq";
9: }
10: private void SetText(string ret)
11: {
12: this.lblResult.Text = ret;
13: }
14: private void btnLongTime_Click(object sender, EventArgs e)
15: {
16: Func<string> func = () => QueryDataBase();
17: //从数据库里获取结果后会更新lblResult这个Label的Text
18: func.BeginInvoke((result) => {
19: string ret = func.EndInvoke(result);
20: //注意这里调用Control.BeginInvoke
21: this.BeginInvoke(new Action<string>(SetText), ret);
22: }, null);
23: }
OK,运行,异常消失的无影无踪。我们在QuertyDataBase方法里的第8行,SetText方法里,也就是上面的第12行设置断点,再运行,然后打开VS的Threads窗口(打开方法:Debug菜单->Windows->Threads),会发现QueryDataBase运行在一个Worker Thread里,不同于Main Thread,而SetText却在Main Thread线程里运行:
上图是命中QueryDataBase中的断点的Threads窗口
上图是命中SetText方法中的断点时Threads窗口,看看黄色箭头所指的地方,哈哈,SetText运行在与UI同一个线程里,没有另起灶炉,这是怎么办到的呢?请看下一节。
发短信的方法知道了,想不想知道发短信的原理?如果不想知道您可以离开了。
没有别的什么招儿,还是上Reflector。
我们发现,不管是Invoke还是BeginInvoke,都是调用
this.FindMarshalingControl().MarshaledInvoke(this, method, args, true/false);
这样的方法,只是第三个参数有不同,通过第三个参数的名字synchronous也猜测的出来是什么意思了。再来阅读MarshaledInvoke方法的代码“去掉干扰部分,取其精华”,差不多就明白了大致流程。
构建一个ThreadMethodEntry
ThreadMethodEntry entry = new ThreadMethodEntry(caller, method, args, synchronous, executionContext);
然后将其添加到Control的theadCallbackList队列中。
然后使用Win32 API RegisterWindowMessage注册一个message(关于message可参见第一篇文章)。
threadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage");
threadCallbackMessage是一个整型值,这个在后面会有用处。
MSDN对RegisterWindowMessage方法的介绍:
RegisterWindowMessage Function
The RegisterWindowMessage function defines a new window message that is guaranteed to be unique throughout the system. The message value can be used when sending or posting messages.
然后使用PostMessage将该消息发送到主窗体,这就是我们说的“发短信”。
这里的PostMessage和前面介绍的SendMessage是孪生兄弟,但亦有一些不同。
引用MSDN的介绍:
SendMessage Function
Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified window and does not return until the window procedure has processed the message.
PostMessage Function
The PostMessage function places (posts) a message in the message queue associated with the thread that created the specified window and returns without waiting for the thread to process the message.
稍微解释下:SendMessage向窗体发送一个消息,然后一直等待,直到指定的窗体处理完该消息后才返回,而PostMessage消息不同,其将消息发送到创建窗体的线程的消息队列上,然后理解返回而不去等待该消息是否处理完。
不管是Invoke还是BeginInvoke,都是使用PostMessage。不同的是Invoke会使用this.WaitForWaitHandle(entry.AsyncWaitHandle);等待消息处理结束。而BeginInvoke立即返回。
上面只是介绍了发送消息,那消息是怎么被处理的呢?
还记得前面介绍的WndProc方法么,Control里的WndProc方法就是“短信处理中心”了。所有发送到窗体的短信默认(因为WndProc是虚方法,可被覆盖)都会在这里处理。要了解更多WndProc方法的细节,参见前面的文章。
在WndProc里我们发现了这样的代码:
1: if ((m.Msg == threadCallbackMessage) && (m.Msg != 0))
2: {
3: this.InvokeMarshaledCallbacks();
4: }
这里的threadCallbackMessage不就是刚才注册消息时的返回值么。
哦,现在思路基本上理清楚了,先注册个消息,然后发送给窗体线程,让窗体线程自己处理设置控件属性的方法,这样就不会出现线程安全的问题了。