初级代码游戏的专栏介绍与文章目录-CSDN博客
我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。
这些代码大部分以Linux为目标但部分代码是纯C++的,可以在任何平台上使用。
源码指引:github源码指引_初级代码游戏的博客-CSDN博客
上一篇VSTO(C#)Excel开发11:自定义任务窗格与多个工作簿-CSDN博客
之前我们已经解决了区分多个任务窗格的问题,现在来考虑一下后台处理的问题。如果有一个任务比较耗时,导致界面长时间没反应的话用户体验就太差了。
解决这种问题的标准方案之一是多线程,起一个新线程来处理,不影响界面操作。
目录
一、界面线程(UI线程)和工作者线程
二、工作者线程与界面交互
三、C#的委托与Invoke
四、Excel里面的多线程问题
4.1 工作者线程中不能操作和创建界面对象
4.2 工作者线程中Workbook与界面线程的不是同一个对象
4.3用委托执行在退出时可能存在异常
一、界面线程(UI线程)和工作者线程
界面线程这个概念是伴随着图形界面出现的。
Windows操作系统上应用程序运行消息循环、处理界面的就是界面线程,默认情况下应用程序只有一个线程,也就是主线程,主线程就是界面线程。
如果用代码创建一个新线程,这个线程就是工作者线程(除非用一些专门的方法把线程转变为界面线程)。工作者线程不处理消息循环,一般用来做与界面无关的耗时任务。
二、工作者线程与界面交互
虽然理论上工作者线程做与界面交互无关的事情,但是难免要与界面交互,比如汇报进度,甚至创建一个对话框出来。
由于Windows和其他图形界面的机制问题,在工作者线程进行界面操作会产生错误(有些情形不是必然出错)。
新手常常很困惑,“为什么写成线程就崩溃了?”
一般异常消息是这样:
没有什么为什么,我们只能牢记这一点:工作者线程不能直接进行界面操作。
既然不能直接操作,那么需要操作的时候怎么做呢?
一种标准方案是发送消息,消息会通过消息机制到达界面线程(额外的提示:Windows上的SendMesssage和PostMessage的行为和是否是同一线程相关的)。
还有一种方案是定时轮询,就是在界面线程的定时查询后台任务的状态(比如检查变量的值)。这种方案虽然很low但是简单可靠,现实中很常用(但是我们不会跟你说是这样做的)。
创建线程的方法很简单,用C#的标准方法即可:
thread = new Thread(thread_OnTime);thread.Start();private void thread_OnTime(){DateTime dateTime = DateTime.Now;//MessageBox.Show("1");while (!bExit){try{if (null != this.Application.ActiveWorkbook && (DateTime.Now - dateTime).TotalMilliseconds >= 5000){Form_Log.CallAddInfo(DateTime.Now.ToString());dateTime = DateTime.Now;//MessageBox.Show(dateTime.ToString());}else Thread.Yield();}catch (Exception ex) {MessageBox.Show(ex.ToString());return;}}//MessageBox.Show("2");}
这段代码每五秒输出一次时间。用了一个属性bExit来指示线程退出。
如果Form_Log.CallAddInfo的实际内容是按照后面写的Invoke方式是安全的,不会引发异常,如果是直接界面操作就会引发上面的那种异常。
三、C#的委托与Invoke
在Windows上运行的程序Windows的消息机制当然是可以用的,不过C#提供了一些更简单的方法:Invoke方法。
Invoke方法用来在界面对象所属的界面线程上执行代码。也就是说当你调用一个对象的Invoke方法时会切换到界面线程上去执行,Invoke方法的参数一般是一个委托(delegate),委托的概念就相当于C语言的函数指针或者脚本语言的函数名。
这样在工作线程上调用界面功能就归结为在界面对象上编写委托然后在工作者线程里Invoke。
示例:
public partial class Form_Log: Form{public delegate void delegateAddInfo(string message);//定义代理类型public delegateAddInfo AddInfo;//定义代理类型的变量private void _AddInfo(string message)//内部的实际功能{。。。。。。//界面操作,子控件也可以,因为都是同一界面线程的}public void CallAddInfo(string message)//方便外部调用,不需要暴露代理类型和代理变量{this.Invoke(AddInfo, message);//在外部直接执行这一句也是一样的,但写起来费劲一点}public Form_Log(){InitializeComponent();AddInfo = new delegateAddInfo(_AddInfo);//设置代理变量}
其实好麻烦的,为什么不直接Invloke(函数指针,参数)啊。
四、Excel里面的多线程问题
4.1 工作者线程中不能操作和创建界面对象
这是通用问题,在此强调一下,因为非常容易疏忽,特别是偶发问题,不容易一下子想到是因为进行了界面调用,以及层层调用的函数调用了界面功能(这就坑死人了,写的人想不到,用的人也想不到)。
不过一般会有异常抛出,所以程序里面多多捕获异常是没错的。
4.2 工作者线程中Workbook与界面线程的不是同一个对象
困惑吧,同样的代码在不同线程里得到的不是同一个对象。
在上一篇我们用了一个字典来管理多个工作簿的任务窗格,用的key是Workbook,在不使用多线程时工作得很好,但是在工作者线程里直接导致了崩溃。
跟踪程序发现问题很诡异:
第一个红框的m_Panels.ContainsKey(workbook)永远是不成立,因此执行到第二个红框报异常。我们已经知道在工作者线程不可以创建界面对象,所以已经提前创建了界面对象,因此按照常理第一个红框的m_Panels.ContainsKey(workbook)应该是成立的(在界面线程里就是成立的)。
调用参数workbook都是用Globals.ThisAddIn.Application.ActiveWorkbook获得的。
实测传入的workbook的Name也是正确的,所以我只能猜测:由于线程不同,同样的Globals.ThisAddIn.Application.ActiveWorkbook返回的workbook是不一样的,但是内部确实又指向了同一个工作簿。具体如何实现的?内部还有指针?还是一个完全不同的只读对象?
具体内部实现先不深究,一切操作统统转到界面线程就没问题了。
4.3用委托执行在退出时可能存在异常
这个在微软文档也有提及,我确实也在执行中发现过。不过影响不大。
下一篇 VSTO(C#)Excel开发13:实现定时器-CSDN博客
(这里是文档结束)