浅谈Excel开发:十 Excel 开发中与线程相关的若干问题
采用VSTO或者Shared Add-in等技术开发Excel插件,其实是在与Excel提供的API在打交道,Excel本身的组件大多数都是COM组件,也就是说通过Excel PIA来与COM进行交互。这其中会存在一些问题,这些问题如果处理不好,通常会导致在运行的时候会抛出难以调试的COM异常,从而导致我们开发出的Excel插件的不稳定。
和普通的WinForm程序一样,Excel也是一种STA(Single Thread Apartment)线程的应用程序,Excel插件是寄宿在Excel中运行的,这也就意味着插件也是一种STA线程的应用程序。插件在操作Excel的时候,如果是在Excel的主线程中,可以直接获取Excel对象进行操作,比如写入单元格值,对单元格进行格式化等操作。但是通常,我们会在多线程或者后台工作线程中去处理一系列复杂的数据或者逻辑,待处理完成获得结果之后,再像WinForm那样,回到UI线程中,去更新界面信息,对于Excel插件来说,就是回到Excel的主线程上来,然后再更新界面。但是Excel又是一种不同于一般Winform 类型的STA,它是COM并且Excel插件是寄宿在其上的,所以还有一些需要注意的地方。
本文首先介绍什么是STA应用程序及其工作原理,然后介绍一般的Winform程序的界面刷新逻辑,以及在这其中非常重要的一个名为SynchronizationContext对象,最后介绍在Excel插件中如何获取Excel主线程,以及这其中需要注意的地方。
Excel插件的最难处理的地方在于其应用程序的稳定性,了解了Excel中的线程以及其机制对增强系统的稳定性会有很大的帮助。
1. STA(Single Thread Apartment)
COM组件的线程模型被称之为Apartment模型,即COM对象初始化时其执行上下文(Execution Context),他要么和单个线程关联STA(Single Thread Apartment ) 要么和多个线程关联MTA(Multi Thread Apartment)。
通常COM对象为了保护其自身维护的数据不被破坏,需要运行时来保证其不被多个线程同时调用;另外也需要运行时来保证对COM对象的调用不会阻塞UI线程。Apartment 就是COM对象生存的地方,一个Apartment可以包含一个或者多个线程。对一个COM对象的调用可以由该COM生存的Apartment中的任何一个线程接受和处理。如果一个Apartment中只有一个线程,那么就是STA线程,否则就是MTA,这个是在程序初始化COM组件的时候即确定下来的。一个进程可以包含多个STA,但是只有一个MTA。
STA模型是COM对象使用的一种非线程安全的模型,这意味着他不能处理自己的线程同步,通常在UI组件中使用这种模型。因此,如果其他线程需要和UI对象进行交互,需要将消息封送(marshall)到STA线程中。在Windows 窗体应用程序中,这一过程是通过窗口消息队列 (message pumping system)来实现的。当客户线程以STA 模式启动时,系统将为STA创建一个隐藏窗口类,所有的对COM对象的调用都会放到这个隐藏窗口的消息队列中。
如果COM对象能够处理其本身的同步逻辑,那么就是MTA模型了,他似的多个线程能够同时和对象进行交互,而不需要进行消息调用的封送。
COM组件在创建的时候采用哪种模型,可以在注册表项的ThreadingModel值中指定:
COM组件在注册表项中的ThreadingModel属性中会有一下四个属性:
- Main thread. COM对象创建 在宿主程序的主UI线程上,所有的调用必须封送到 主UI线程上 .
- Apartment. 表示该COM对象能够运行在任何但单线程模型的线程上,如果该线程是STA线程创建的,则对象运行在该STA线程上,否则该对象运行在主STA线程上,如果主STA线程不存在,系统则会自动创建一个。
- Free. 表示该COM对象运行在MTA上。
- Both. 表示该COM对象在那个模型上取决于创建Apartment的类型。
对于.NET Framework来说,通常在任何创建UI的线程上使用[STAThread]自定义属性来标识其为STA线程。工作线程通常使用MTA模型,但是如果该工作线程需要与表示为Apartment的COM组件一起使用,那就需要标识为STAThread。
我们可以给Thread对象的ApartmentState属性指定ApartmentState枚举类型来给定该Thread属于那种类型的线程。
那么如何在其他线程中往STA线程中封送消息呢?这个就要使用SynchronizationContext对象了。
2. SynchronizationContext
关于SynchronizationContext类,Understanding SynchronizationContext (Part I) 这篇文章讲解的比较好,建议直接阅读原文。这里简要说一下,为后面讲解做铺垫。 SynchronizationContext类主要是用来进行线程间进行通讯的, 比如我有Thread1和Thread2,Thread1在做一些事情,完了之后,Thread1希望将结果传递给Thread2,希望在Thread2上执行操作。一种可行的方式是获取Thread2的SynchronizationContext对象,然后在Thread1中调用SynchronizationContext的Send或者Post方法,这样需要做的操作就会在Thread2上执行的。需要注意的是,并不是所有的线程都有一个SynchronizationContext与之联系,只有UI线程上才有SynchronizationContext,通常是在线程中,第一次创建UI控件的时候,就会将SynchronizationContext对象附加到当前的线程中。
在进行Winform开发的时候,我们知道不应该在UI线程上执行耗时的操作,因为UI线程是一种STA线程,是通过消息队列来实现的,如果某一操作耗时的话会阻塞其他的消息处理,影像用户交互。所以我们一般需要将一些耗时操纵放到后台线程中去处理,完了之后将结果Post回UI线程来进行界面刷新,我们常在非UI线程中使用Control的Invoke和BeginInvoke来实现UI界面的刷新。而Invoke和BeginInvoke在内部其实是通过继承自SynchronizationContext的对象来发送消息实现的。
通常,可以通过SynchronizationContext.Current的静态属性来获取当前线程的SynchronizationContext对象
有了UI线程的SynchronizationContext对象我们就可以在其他线程上通过该对象将我们需要在UI线程上进进行的操作Post到UI所在的线程上的消息队列中了。
下面的代码中我们在button2中新建了一个新的进程,然后在该进行的方法中传入了当前UI线程的SynchronizationCotext对象, 然后在工作线程中通过该SynchronizationContext对象的Post方法更新UI界面上的Combox对象:
- private void button2_Click(object sender, EventArgs e)
- {
- // let's see the thread id
- int id = Thread.CurrentThread.ManagedThreadId;
- Trace.WriteLine("Button click thread: " + id);
- // grab the sync context associated to this
- // thread (the UI thread), and save it in uiContext
- // note that this context is set by the UI thread
- // during Form creation (outside of your control)
- // also note, that not every thread has a sync context attached to it.
- SynchronizationContext uiContext = SynchronizationContext.Current;
- // create a thread and associate it to the run method
- Thread thread = new Thread(Run);
- // start the thread, and pass it the UI context,
- // so this thread will be able to update the UI
- // from within the thread
- thread.Start(uiContext);
- }
- private void Run(object state)
- {
- // lets see the thread id
- int id = Thread.CurrentThread.ManagedThreadId;
- Trace.WriteLine("Run thread: " + id);
- // grab the context from the state
- SynchronizationContext uiContext = state as SynchronizationContext;
- for (int i = 0; i < 10; i++)
- {
- // normally you would do some code here
- // to grab items from the database. or some long
- // computation
- Thread.Sleep(10);
- // use the ui context to execute the UpdateUI method,
- // this insure that the UpdateUI method will run on the UI thread.
- uiContext.Post(UpdateUI, "line " + i.ToString());
- }
- }
- /// <summary>
- /// This method is executed on the main UI thread.
- /// </summary>
- private void UpdateUI(object state)
- {
- int id = Thread.CurrentThread.ManagedThreadId;
- Trace.WriteLine("UpdateUI thread:" + id);
- string text = state as string;
- comboBox1.Items.Add(text);
- }
运行结果如下,我们可以看到Button以及UpdateUI的方法都是在UI线程上运行的,他们具有相同的线程ID 9,而我们新建的工作线程ID为10。
- Button click thread: 9
- Run thread: 10
- UpdateUI thread:9
- UpdateUI thread:9
- UpdateUI thread:9
- UpdateUI thread:9
- UpdateUI thread:9
- UpdateUI thread:9
- UpdateUI thread:9
- UpdateUI thread:9
- UpdateUI thread:9
- UpdateUI thread:9
SynchronizationContext对象有Send和Post两个方法可以被我们调用。Send方法是同步的,他会等待Send进去的代理方法执行完成之后,再执行后面的代码,而Post方法则是异步的,Post之后会继续执行后续的代码,Post和Send方法会异常的捕获。其内部的实现大致如此:
- public virtual void Send(SendOrPostCallback d, Object state)
- {
- d(state);
- }
- public virtual void Post(SendOrPostCallback d, Object state)
- {
- ThreadPool.QueueUserWorkItem(new WaitCallback(d), state);
- }
实际上在Winform以及WPF中,我们获取到的是继承自SynchronizationContext的对象,在Winform中是System.Windows.Forms.WindowsFormsSynchronizationContext在WPF中则是 System.Windows.Threading.DispatcherSynchronizationContext。比如在Winform中是Control.BeginInvoke,在WPF或者Silverlight中是Dispatcher.BeginInvoke,这些类都重写了Post和Send方法。并提供了各自的“消息队列”(message pump)机制比如Windows API中的SendMessage 和PostMessage方法来实现各自的消息分发和处理。我们在上面代码中通过SynchronizationContext的Current获取到的实际上是一个WindowsFormsSynchronizationContext对象。真实的SynchronizationContext类不做任何实现,他更应该是一个虚类。所以通过手动new一个SynchronizationContext,然后赋予当前的线程是没有任何意义的。
3. Excel中的线程同步
前面讲过STA以及SynchronizationContext,这是因为Excel也是一种STA线程的应用程序,寄宿在Excel之上的Automation程序也是STA的,了解这一点非常重要。
通常在Excel的插件开发中,我们的业务逻辑可能比较复杂,这些复杂的计算一般不应该放到Excel的主UI线程中,我们需要新建工作线程,然后在里面进行计算。获得了结果之后,我们应该在回到Excel的UI线程中去更新界面。但是我们采用.NET技术开发Excel的Automation有一个特殊性在于,我们可以直接在非UI线程中去调用Excel的COM对象,在正常情况下,如果Excel比较空闲,没有任何问题,但是如果Excel此时比较忙,就会抛出COM异常,这种异常难以捕捉。这也是导致插件不稳定的一个非常重要的因素。这种情况通常出现在以下情形中:
- 当我们的插件在后台线程中向服务端请求了大量数据,进行了一些处理(这种情况很常见)后,在Excel的Sheet页中将数据填充到单元格中,然后对单元格进行样式,字体等格式化,这个过程需要与COM进行交互,而且在某些情况下比较耗时,如果在此过程中,用户操作了Excel的单元格,比如鼠标点击填充过程中的单元格,这样由于后台线程通过COM对象对Excel的操作会遇到忙碌状态,就会抛出COM异常。
- 在RTD 函数中,我们在某些情况下需要定时刷新单元格,比如在Excel中直播NBA比赛得分,使用实时的股票市场行情信息进行建模。在RTD 中,我们可以直接调用UpdateNotify方法,通常该方法应该在UI主线程上调用,这样Excel就会将其放到消息队列中,在某一时候触发。但是在很多时候,我们获取数据比如NBA实时比分,实时行情数据,通常是在另外一根工作线程中进行的,我们可以在工作线程中直接调用RTD的UpdateNofity方法,但是在Excel忙碌的时候就会抛出COM异常。
Excel中运行我们再工作线程中通过Excel 的Application对象来直接更新UI界面元素给了我们一个假象。原因在于这样是很不稳定的,非Excel主线程的每一次COM调用中都需要检查是否抛出异常,在调用过程中Excel很可能处于忙碌状态,Excel也可能在任何情况下拒绝线程对COM调用的请求,尤其是在用户正与Excel进行交互的时候。通常我们至少要捕获和处理一下三种COM异常:
- u const uint RPC_E_SERVERCALL_RETRYLATER = 0x8001010A;
- u const uint VBA_E_IGNORE = 0x800AC472;
- u const uint RPC_E_CALLREJECTED
在其他线程中直接调用Excel对象不仅会导致性能损失,而且会增加插件的复杂性和不稳定性。
正确的做法是,在工作线程中获取Excel主线程对象的SynchronizationContext,然后将待操作的步骤Post到Excel主线程的消息队列中等待处理。但是作为一个Addin,在一般情况下如果直接获取SynchronizationContext对象,该对象是为空的,只有在插件加载后,手动创建一个Winform窗体或者控件才能够获取到主线程的SynchronizaitonContext对象。这个From窗体通常就是我们插件的登录窗体。
比如如果要在非Excel 主线程中调用RTD函数的UpdateNotify方法,我们可以首先定义一个SynchronizationContext用来保存Excel主线程的同步上下文。
- private SynchronizationContext ExcelContext;
然后在RTD启动时获取当前Excel主线程的上下文。
- public int ServerStart(IRTDUpdateEvent CallbackObject)
- {
- this.ExcelContext = new SynchronizationContext();
- xlRTDUpdater = CallbackObject;
- }
最后工作线程中,通过传进来的ExcelContext,然后将需要做的操作Send或者Post回Excel主线程中执行。
- ExcelContext.Post(delegate(object obj)
- {
- xlRTDUpdater.UpdateNotify();
- }, null);
所以其他非UI线程中需要操作Excel COM对象的方法经过如此封装将需要做的操作以消息的形式封送到UI线程,这样就可以解决之前调用COM组件可能出现的COM异常,能够极大提高Excel插件的稳定性。
本文很多内容涉及到COM组件的相关知识,这里只是简单的讲解了一些与Excel插件开发中可能与之相关的一些问题,介绍了如何正确的在工作线程中更新Excel UI操作的一些正确做法,希望这些知识对您有所帮助。
参考资料
本文参考了很多资料,如果您想深入了解,以下文章对您或许有帮助。
- Understanding The COM Single-Threaded Apartment
- Understanding SynchronizationContext
- Apartments and Pumping in the CLR
- What is a message pump?
- Excel interop COM exception while running in background
- Accessing Excel Application Object From An STA Thread (Refer to the post by Geoff Darst)
- ExecutionContext vs SynchronizationContext
- A CONCRETE EXAMPLE OF HOW CONTROL.INVOKE CAN CAUSE DEADLOCK
浅谈Excel开发:十 Excel 开发中与线程相关的若干问题的更多相关文章
- 浅谈利用同步机制解决Java中的线程安全问题
我们知道大多数程序都不会是单线程程序,单线程程序的功能非常有限,我们假设一下所有的程序都是单线程程序,那么会带来怎样的结果呢?假如淘宝是单线程程序,一直都只能一个一个用户去访问,你要在网上买东西还得等 ...
- 示例浅谈PHP与手机APP开发,即API接口开发
示例浅谈PHP与手机APP开发,即API接口开发 API(Application Programming Interface,应用程序接口)架构,已经成为目前互联网产品开发中常见的软件架构模式,并且诞 ...
- 【ASP.NET MVC系列】浅谈jqGrid 在ASP.NET MVC中增删改查
ASP.NET MVC系列文章 [01]浅谈Google Chrome浏览器(理论篇) [02]浅谈Google Chrome浏览器(操作篇)(上) [03]浅谈Google Chrome浏览器(操作 ...
- Python 浅谈编程规范和软件开发目录规范的重要性
最近参加了一个比赛,然后看到队友编程的代码,我觉得真的是觉得注释和命名规范的重要性了,因为几乎每个字符都要咨询他,用老师的话来说,这就是命名不规范的后续反应.所以此时的我意识到写一篇关于注释程序的重要 ...
- 浅谈移动应用的跨平台开发工具(Xamarin和React Native)
谈移动应用的跨平台开发不能不提HTML5,PhoneGap和Sencha等平台一直致力于使用HTML5技术来开发跨平台的移动应用,现在看来这个方向基本算是失败的,基于HTML5的移动应用在用户体验上与 ...
- 浅谈 PHP 与手机 APP 开发(API 接口开发) -- 转载
转载自:http://www.thinkphp.cn/topic/5023.html 这个帖子写给不太了解PHP与API开发的人 一.先简单回答两个问题: 1.PHP 可以开发客户端? 答:不可以,因 ...
- 浅谈 PHP 与手机 APP 开发(API 接口开发)
本文内容转载自:http://www.thinkphp.cn/topic/5023.html 这个帖子写给不太了解PHP与API开发的人一.先简单回答两个问题:1.PHP 可以开发客户端?答:不可以, ...
- 浅谈PHP与手机APP开发(API接口开发)
了解PHP与API开发 一.先简单回答两个问题: 1.PHP 可以开发客户端? 答:不可以,因为PHP是脚本语言,是负责完成 B/S架构 或 C/S架构 的S部分,即:服务端的开发.(别去纠结 GTK ...
- 浅谈SharePoint 2013 站点模板开发 转载自http://www.cnblogs.com/jianyus/p/3511550.html
一直以来所接触的SharePoint开发,都是Designer配合Visual Studio,前者设计页面,后者开发功能,相互合作,完成SharePoint网站开发.直到SharePoint 2013 ...
随机推荐
- 用命令查看Mysql中数据库、表的空间大小
要想知道每个数据库的大小的话,步骤如下:1.进入information_schema 数据库(存放了其他的数据库的信息)use information_schema;2.查询所有数据的大小:selec ...
- android的单击监听事件
Button button = (Button) findViewById(R.id.button1); //1.直接new出来 button.setOnClickListener(new View. ...
- k-sum问题
给定一个数组,里面的是任意整数,可能有重复,再给定一个目标T,从数组中找出所有和为T的K个数,要求结果中没有重复. Note: Elements in a quadruplet (a,b,c,d) m ...
- 给自己立下一个巨大的flag
[BZOJ1861][BZOJ3224] [BZOJ2733][BZOJ1056] [BZOJ2120][BZOJ3673] [BZOJ1833][BZOJ1026] [BZOJ3209][BZOJ1 ...
- 【dubbo】zookeeper搭建
依赖java JDK,需提前安装1.6及以上版本 1.下载zookeeper (3.4.9) 2.设置配置文件\zookeeper-3.4.9\zookeeper-3.4.9\conf\zoo.cfg ...
- Java多线程理解
首先说一下进程和线程的区别 进程:是计算机运用程序实例,拥有独立的内存空间和数据(猜测内存堆应该是作用的进程上),一个进程包含多个子线程,不同进程相互独立: 线程:cpu执行的基本单位,拥有独立的寄存 ...
- Microsoft Visual Studio 2010 已安装的模板 没有 “ADO.NET实体数据模型”
2010 sp1才包括entity framework. 装一个补丁即可 地址为:http://www.microsoft.com/zh-CN/download/details.aspx?id=236 ...
- Mac OS X双系统变回虚拟机
Mac OS X双系统变回虚拟机 自从装了双系统后,感觉不要太好,装了虚拟机就开始有工作的干劲了.不妙的是,我在Win7系统里并没有装office,用不了word文档就写不了笔记和总结.我不太想在Wi ...
- Windows下Oracle安装图解----oracle-win-64-11g 详细安装步骤
一. Oracle 下载 官方下地址 http://www.oracle.com/technetwork/database/enterprise-edition/downloads/index.htm ...
- NSString格式校验
在项目开发过程中,NSString类型的变量是经常用到的,而且我们常常会对其格式进行对应的各种校验,你比如,在登录注册的时候,需要验证用户名的长度,用户名的字符组成等等,其实现在也有很多第三方提供的N ...