7月25日 星期六 天气晴
今天是我写日志的第14天。也就是说,已经过了两个星期了。是时候好好的总结一下,整理一下了。
不过在开头,还是得把落下的功课补上。
C#窗体多线程编程
首先还是把问题说清楚:
1. 取消按钮根本无响应 2.UI不进行同步更新
那我要做些什么呢?
将备份向导中的备份操作写到另外一个线程里去,通过这个非UI线程对控件进行更新操作。
==========================================================================
首先是问题,究竟是什么原因导致的呢?
当然现在我清楚了,知道是什么原因,但是当时脑袋有点发胀,根本一点思路都没有。所以说还是不够冷静,遇到问题一定要冷静,越难的问题越要冷静地对待。还有一点是,如果实在想不出来,可以看看网页,写写诗,聊聊天,换换脑子,说不定就能出来了。这也是昨天剩半小时找到问题的经验。
那到底是什么问题呢?这个关键还是在我对线程,或者说C#窗体编程的不了解。
其实C#的窗体程序,自运行起就一直在主线程之下。也就是Main函数。通常我们也叫他UI线程,因为UI控件的事件监听都由他负责。(控件的事件监听要去了解一下啊,看看他们写的控件就知道了。)
当然,在VS中调试的时候,也可以看到程序运行时的线程,在视图中选择工具栏,里面的调试,就可以叫出线程监视窗口了。不过我个人感觉几乎没什么用。就是看着清晰一点。
说说我之前写的备份向导。备份操作的代码封装在DataBackup类中,而调用这个类是在ExecutionForm里面,也就是说,备份方法的调用是发生在窗体线程里面的。如果备份方法占用了大量的CPU,那么窗体线程就无法很好地对用户进行响应了。而这,就是取消按钮无法点击,和窗体控件更新无法显示的原因了。
=================================================================================================
感觉问题好像挺简单。就开一个新线程不就OK了吗。
好的,就开一个新线程,开新线程,有好几种方法:
1. 利用异步委托:大体上讲就是定义一个委托,然后利用委托的异步调用方法BeginInvoke。不过执行起来有三种做法:投票,等待句柄和异步回调。这里我就不详细说了,尽管去查查书。(我看的是C#高级编程第6版) 因为这个方法满足不了我的需要:我的目的是可以中途停掉这个线程的,很明显异步委托的线程我无法控制。
2.使用Thread类。使用方法是Thread Thread = new Thread(执行体方法名)。 这个方法明显是可以的。
3.使用BackgroundWorkers,这是VS里自带的一个控件,专门负责在窗体程序中执行非UI计算,并对UI控件进行更新。
===================================================================================================
我首先选用了新开一个线程的方法,问题仍然存在,而且多了另外一个问题:
非窗体线程无法访问该窗体上面的UI控件。
这是.NET为了线程安全设置的一个检测,就是说,控件只有添加它的窗体所在的线程才能访问,其他的线程都无法访问。而备份线程需要实时地对窗体上面的控件进行更新。怎么办呢? BackgroundWorkers是在.NET2.0后,为了解决这个问题专门诞生的。
但是在其诞生之前,还是有两种方法:
1. 将Control.CheckForIllegalCrossThreadCalls属性设为false。这样就免去了不同线程之间访问的检查。不过不推荐这种方法。
2. 第二种方法是利用方法回调。
首先定义一个方法回调的委托: delegate void EventHandlerCallBack(object sender, EventArgs args);
然后在事件处理函数里面这样写,假设事件处理函数是step
void step(object sender, EventArgs args)
{
if (this.invokeRequired == true)
{
EventHandlerCallBack callBackFunc = new EventHandlerCallBack(step);
this.Invoke(callBackFunc,new object[]{sender, args});
}
else
{
这里是你的响应方法原来的代码
.......
}
}
this指的是窗体类。窗体类有这么一个invokeRequired属性,它会判断当前的线程ID跟窗体线程ID是否一致,如果不一致,那就要调用Invoke方法,所以就是true了。
这种方法的意图是,从辅助线程(这里是备份线程)中通过生成一个回调委托,注意红色那句,委托的方法是step自己。然后用this,也就是窗体本身来Invoke这个方法,使得回调后可以进入到else语句块中执行。
这种方法能很漂亮地解决问题。但是Background Woker就直接包含了这些机制,在它里面可以直接访问窗体的控件,更加舒服。但是毕竟是人家写好的控件,灵活性太差了。比如它的取消操作:它有一个CancelAsync的方法,用于取消BackgroundWoker的线程。但是事实上,这个方法只会完成一件事:就是将Background Workers中的CancellationPending属性设为true。然后还需要你在执行体中不断地判断这个属性来决定是否退出线程。。。我相当无言。具体可以参考这篇文章:
http://www.cnblogs.com/virusswb/archive/2008/08/29/1279608.html
所以还是要自己来写一个线程。
自己写了线程,也些了回调了,但还是没有办法令取消按钮响应。窗口也没显示。。。
终于发现问题了:原来是主线程中监听的问题
在我写完thread.start()下面。
我写了这么几句:
while(thread.ThreadState == ....) {}
也就是我用一个 while来不断监听 线程结束了没有。 这个在计算机组成原理课上,应该叫轮询吧。
我也是很自然地就这么写了。因为异步委托的投票方法,也是在BeginInvoke之后,有一个while循环,查询委托是否已完成的。然而很明显那里面是有异步机制进行支持的。
问题就出现在这里了: 主线程不断地运行while循环,几乎占用了全部的CPU时间,哪里还有功夫去处理UI方面的更新呢?正确的方法应该是让线程完成时激发一个完成事件,由窗体类订阅这个事件。也就是说不用轮询,用中断。由备份线程去通知主线程它完成了。
按照这个思路去修改了程序。发现一切都顺当起来了。
虽然花了许多冤枉时间,但是还是值得的,起码对多线程的机制有点理解了。
===============================================================================================
这里还要记录一个奇怪的现象:
当我在vs里面调试的时候,曾经遇到这样的情况:单步调试,一步一步,突然一步之后,调试步点不知道去哪里了。整个程序也不动了。但是没有显示无法响应。而打开线程窗口看。所有线程都消失了。这个错误让我郁闷了好长一段时间:原来是在某段辅助线程的代码中,有一句对窗体控件属性的访问。
可是他也不抛异常,就直挺挺地死掉。。。真是让人无法捉摸。不过记住这个情况就好了。
还要记录一点:
关于线程的挂起与恢复。
可以是用Thread里面的Suspend和Resume方法,不过这两个方法已经被Depricated了。所以还是想想利用别的方法好了。