WinForm二三事(三)Control.Invoke&Control.BeginInvoke
http://www.cnblogs.com/yuyijq/archive/2010/01/11/1643802.html
这个系列从2009年写到2010年,差点又成太监文。随着WPF/Silverlight的袭来,WinForm慢慢的在灭迹,为了纪念我那两年多来开发WinForm的日子,遂撰写此系列,计划包括十篇文章,将我觉得WinForm开发需要注意的一些方面谈谈,前两篇已经写了(实际已经写了三篇),这是本系列第三篇:
WinForm二三事(三)Control.Invoke&Control.BeginInvoke
WinForm二三事(四)界面布局
WinForm二三事(五)实作
WinForm二三事(六)数据绑定
WinForm二三事(七)GDI+
WinForm二三事(八)开源项目
WinForm二三事(九)常用第三方控件库
WinForm二三事(十)漫谈
从异常开始
在上一篇文章中,为了提高用户体验,使用delegate构造一个异步操作,但是在这个异步操作里操作UI控件的属性的时候却发生异常。实际上使用delegate构造异步操作这种方式,在背后还是创建了一个worker thread,从不是创建UI的thread里去操作UI元素的属性就会抛出这个异常。
不过,如果我们不在Visual Studio里运行这个程序,直接运行,这个异常却不会出现。通过查看异常的StackTrace,发现该异常是在获取Control的句柄时抛出的:
at System.Windows.Forms.Control.get_Handle()
at System.Windows.Forms.Control.set_WindowText(String value)
at System.Windows.Forms.Control.set_Text(String value)
at System.Windows.Forms.ButtonBase.set_Text(String value)
//..省略...
祭出Reflector,看看相关代码:
1: public IntPtr Handle
2: {
3: get
4: {
5: if ((checkForIllegalCrossThreadCalls && !inCrossThreadSafeCall) && this.InvokeRequired)
6: {
7: throw new InvalidOperationException(SR.GetString("IllegalCrossThreadCall", new object[] { this.Name }));
8: }
9: if (!this.IsHandleCreated)
10: {
11: this.CreateHandle();
12: }
13: return this.HandleInternal;
14: }
15: }
然后是主要检查checkForIllegalCrossThreadCalls、inCrossThreadSafeCall两个字段以及InvokeRequired,InvokeRequired的职责是判断当前运行的线程是不是与窗体主线程是同一个线程。
通过字面理解checkForIllegalCrossThreadCalls的意思就是“跨线程调用时是不是检查”。而这个字段在Control的静态构造函数里被设置为:checkForIllegalCrossThreadCalls = Debugger.IsAttached(checkForIllegalCrossThreadCalls 还通过CheckForIllegalCrossThreadCalls属性公开了 ),现在明白了为啥在Visual Studio里调试程序报异常,而独立运行却不报了吧。
实际上如果在不是创建控件的线程里设置控件的属性抛出异常是正常的,不抛才不正常,我们不能为了异常而异常,如果多个线程访问控件属性,可能会有线程安全的问题,造成控件运行不稳定。那解决之道是什么?
只在创建控件的线程里设置控件的属性
废话,我当然知道这样是可以的,但是两个线程都在自顾自的运行,我有什么办法让别的线程停下手头的工作,来执行我分派的任务,这又不是两个人,我可以跟他说下。嘿,您还别说,这里的机制还真像两个人,甲线程发个短信给乙线程说,哥们,我有个事儿自己不好处理,怕出问题,你抽空儿给我处理下。根据甲线程发短信的方式,甲线程要么发了短信后就忙自己的事儿然后等乙线程处理完后的消息,要么一直在那里傻傻的等待着乙线程处理完。
Control.Invoke&Control.BeginInvoke
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同一个线程里,没有另起灶炉,这是怎么办到的呢?请看下一节。
发短信的方法知道了,想不想知道发短信的原理?如果不想知道您可以离开了。
PostMessage
没有别的什么招儿,还是上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
还记得前面介绍的WndProc方法么,Control里的WndProc方法就是“短信处理中心”了。所有发送到窗体的短信默认(因为WndProc是虚方法,可被覆盖)都会在这里处理。要了解更多WndProc方法的细节,参见前面的文章。
在WndProc里我们发现了这样的代码:
1: if ((m.Msg == threadCallbackMessage) && (m.Msg != 0))
2: {
3: this.InvokeMarshaledCallbacks();
4: }
这里的threadCallbackMessage不就是刚才注册消息时的返回值么。
哦,现在思路基本上理清楚了,先注册个消息,然后发送给窗体线程,让窗体线程自己处理设置控件属性的方法,这样就不会出现线程安全的问题了。
后记
本文介绍了Control.Invoke&Control.BeginInvoke背后运行的机制,这主要是依靠RegisterWindowMessage以及PostMessage这两个Win32 API办到的。
还要注意的是多线程是实现异步操作的一种方式,但异步操作并不就是多线程,比如这里的Control.BeginInvoke的实现方法。
WinForm二三事(三)Control.Invoke&Control.BeginInvoke的更多相关文章
- (转)c# control.Invoke control.BeginInvoke
在Invoke或者BeginInvoke的使用中无一例外地使用了委托Delegate. 一.为什么Control类提供了Invoke和BeginInvoke机制? 关于这个问题的最主要的原因已经是do ...
- WinForm Control.Invoke&Control.BeginInvoke异步操作控件实例
参考:http://www.cnblogs.com/yuyijq/archive/2010/01/11/1643802.html 效果图: 实例(实验)代码: using System; using ...
- c#多线程(UI线程,控件显示更新) Invoke和BeginInvoke 区别
如果只是直接使用子线程访问UI控件,直接看内容三,如果想深入了解从内容一看起. 一.Control.Invoke和BeginInvoke方法的区别 先上总结: Control.Invoke 方法 (D ...
- [转] c#多线程(UI线程,控件显示更新) Invoke和BeginInvoke 区别
如果只是直接使用子线程访问UI控件,直接看内容三,如果想深入了解从内容一看起. 一.Control.Invoke和BeginInvoke方法的区别 先上总结: Control.Invoke 方法 (D ...
- C#中的Invoke和BeginInvoke
一.Control#Invoke() 和Control#BeginInvoke() 在非UI线程中调用MessageBox.Show()结果是非模态对话框: 在UI线程中调用MessageBox.Sh ...
- C#中的线程二(Cotrol.BeginInvoke和Control.Invoke)
C#中的线程二(Cotrol.BeginInvoke和Control.Invoke) 原文地址:http://www.cnblogs.com/whssunboy/archive/2007/06/07/ ...
- [转载]Winform中Control的Invoke与BeginInvoke方法
转自http://www.cppblog.com/baby-fly/archive/2010/04/01/111245.html 一.为什么 Control类提供了 Invoke和 BeginInvo ...
- C# WinForm多线程(三)Control.Invoke
下面我们就把在Windows Form软件中使用Invoke时的多线程要注意的问题给大家做一个介绍. 首先,什么样的操作需要考虑使用多线程?总的一条就是,负责与用户交互的线程(以下简称为UI线程)应该 ...
- Control的Invoke和BeginInvoke详解
(一)Control的Invoke和BeginInvoke 我们要基于以下认识: (1)Control的Invoke和BeginInvoke与Delegate的Invoke和BeginInvoke是不 ...
随机推荐
- VR电竞游戏在英特尔®架构上的用户体验优化
作为人与虚拟世界之间的新型交互方式,VR 能够让用户在模拟现实中获得身临其境的感受.但是,鉴于 VR 的帧预算为每帧 11.1ms (90fps),实现实时渲染并不容易,需要对整个场景渲染两次(一只眼 ...
- HPUX修改disk实例号--11.31only
有时由于一些原因或者用户的要求,需要修改Disk的实例号,这里简单介绍如何手工进行修改. 在修改之前需要做一些准备工作,即先将stale状态的设备文件清理掉,具体步骤如下: 使用ioscan命令列出s ...
- H2O Driverless AI
H2O Driverless AI(H2O无驱动人工智能平台)是一个自动化的机器学习平台,它给你一个有着丰富经验的“数据科学家之盒”来完成你的算法. 使AI技术得到大规模应用 各地的企业都意识到人工智 ...
- mac 安装配置使用nexus3.x
一.nexus安装 前置条件 :已经安装了JDK 1:下载nexus(http://www.sonatype.com/download-oss-sonatype) 最新版本3.0,下载目录为/User ...
- [Algorithm] A* Search Algorithm Basic
A* is a best-first search, meaning that it solves problems by searching amoung all possible paths to ...
- app开发相关
app播放UIWebview 没有声音解决: 设置 allowsInlineMediaPlayback = YES; mediaPlaybackRequiresUserAction = NO
- php分页类学习
分页是目前在显示大量结果时所采用的最好的方式.有了下面这些代码的帮助,开发人员可以在多个页面中显示大量的数据.在互联网上,分页是一般用于搜索结果或是浏览全部信息(比如:一个论坛主题).几乎在每一个W ...
- Factorials 阶乘(思维)
Description N 的阶乘写作N!表示小于等于N的所有正整数的乘积.阶乘会很快的变大,如13!就必须用32位整数类型来存储,70!即使用浮点数也存不下了.你的任务是 找到阶乘最后面的非零位.举 ...
- asp.net如何实现跟踪检查用户知否查看了邮件。
有时我们有这样一种需求场景,我们给很多用户发了邮件,需要一个反馈,用户是否查看了我们发送的邮件,百度了以下果然有方案. 我总结实践了下这个过程,同时有自己的一点使用感受.记录下希望对你有帮助. 有人想 ...
- 运维学习笔记(七)之T02-01计算机网络 、 数制 、 网络通信参考模型
计算机网络 计算机网络概述 什么是计算机网络 硬件方面:通过线缆将网络设备和计算机连接起来 软件方面:操作系统.应用软件.应用程序通过通信线路互连 实现资源共享.信息传递 功能 数据通信/资源共享/增 ...