C#学习纪要(14):7月25日

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了。所以还是想想利用别的方法好了。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值