1.背景

第一次做实时监控程序的时候,可能都会和博主一样,遇到界面刷新速度很慢,或者说界面响应速度很慢的情况,而且很难一下子断定问题的关节。刚遇到这个问题的时候,按照学过的算法优化基础,做了一些简单的计算优化,比如:把多次创建图片的地方改为共享同一个图片,看看有没有for循环的地方有不必要的计算、或者多次文件读取。接下来在百度、谷歌搜索,使用了“MVC multi thread “、“realtime system“、“realtime software”这样的关键词。依然没有找到需要的答案,或许“很多软件的问题的答案本就不在网上,而是在自己的代码里”。

在接下来的代码检查和修改过程中总结了实时监控软件开发可以借鉴的4个注意事项。

2.基本优化

重点关注涉及界面元素属性修改以及绘制界面元素的代码。看图片创建、文件访问、对象创建等操作可以调整到全局变量进行一次创建,共享使用。看计算、循环等处是否存在不当的地方。比如,之前开发过一个依据数据库表内容创建Excel报表的程序,需要用表格模板中的所有单元格值与数据库表中的key列比较,相同时则用value列的值替换表格模板的单元格值。使用了双循环,Excel单元格遍历的循环在内,速度极慢3-4分钟,生成一个50行10列的报表,改为Excel单元格遍历的循环在外后,时间变为10几秒。

另外比较重要的一点,就是界面线程操作语句要尽量少,把计算任务托付给后台线程。在C#里面后台线程和前台界面链接的关键点就是Invoke和BeginInvoke,如果后台线程想要修改界面的元素必须使用两者之一,否则你就会遇到“the calling thread can not access this object because a different thread owns it”的异常。可能,有时候就算你已经使用了Invoke或者BeginInvoke,但是程序仍然报出上述异常,这种情况将在第3节中说明解决思路。减少界面线程操作语句数量,也就意味着减少这两个语句调用方法的操作语句数量。

对于基本优化,博主认为以上两点应该足够了,其他的除法改为乘法,使用更高级的算法等等对于非服务器程序意义不是很大。

3.Invoke和BeginInvoke

可能在程序里,你已经使用了Invoke和BeginInvoke但是仍然报出“不在创建线程使用控件”的异常,这时候该怎么解决呢?

先简单的说一下Invoke和BeginInvoke的区别。Invoke是阻塞方式的调用,即后台线程会等待界面线程操作完成才继续后续的代码执行。BeginInvoke是非阻塞方式的调用,即后天线程把调用转给界面线程之后,立即退出,继而执行后续的代码。需要提醒的一点是,不能简单的看待这个不同点,任务他们对实际的编程来说没有实质的影响。其实不然,在多线程编程中,大家都明白最让人头疼的问题就是访问冲突问题。如上两种方式的调用,前者对于Invoke中执行的代码不会同时调用两次,然而BeginInvoke中的代码就很可能被调用两次,也就是说BeginInvoke潜在着一个访问冲突的可能性。这一点不管之前注意到没有,在遇到问题的时候,应该要把这点考虑在内。

言归正传,对于这一节提到的异常,原因仍然是在第二个线程调用了其他线程创建的界面元素。比如,C# WPF做一个图片轮换显示程序,使用Image对象作为显示载体,使用后台想成逐张图片读取。Image对象由界面线程创建,给Image.Source赋值的语句需要由Invoke包被。为了尽量减少界面线程的处理负荷,由后台线程创建ImageSource对象,然后调用Invoke将ImageSource对象赋予Image.Source。此时,虽然修改界面元素的语句由Invoke调用,但是仍然报出“the calling thread can not access this object because a different thread owns it”的错误。为什么?

直观的感觉都是:界面的元素被后台线程直接访问了,对吧。其实,这只是一个方面,在这个场景下就是界面线程访问了由后台线程创建的界面元素(一般指System.Windows名空间中的类)。

解决方法就是,把ImageSource的创建也包被到Invoke里面。用过wpf的读者可能会说,ImageSource的创建完全没有必要调用后台线程,使用System.Windows.Forms.Timer或者System.Windows.Threading.DispatcherTimer就可以了。是的,如果仅仅是一般的读取jpg、bmp是没有问题的,但是如果是j2k/jp2等其他压缩格式,需要进行解压等操作的时候就有所不同了。

4.关于Timmer

不论用什么语言编程,Timmer对象应该都不会陌生。这里仅谈C#里面的Timmer对象。C#里面的Timmer对象分为两种:多线程和单线程,多线程Timmer类是System.Threading.Timmer和System.Timers.Timer,单线程的Timmer类是System.Windows.Forms.Timer(Windows Forms Timmer)和System.Windows.Threading.DispatcherTimer(WPF Timmer)。

两者的主要区别是以下两点:

1.单线程Timmer和界面线程是同一个线程,可以直接访问界面元素,修改界面元素值。多线程的Timmer与界面不是同一个线程。如果,你写了一个显示时间累加的桌面程序,鼠标按住程序的标题栏拖动,时间不动了,那么你用的就是一个单线程的Timmer了。

2.单线程Timmer注册的OnTimmer方法执行完成之后,下一个Tick事件才会触发。或者说,前一个OnTimmer方法没有执行完时,下一个Tick事件绝不会触发。多线程的Timmer会在每一个确切的时间点触发Tick事件,每个事件由线程池中的一个线程执行。

如果想了解更多Timmer的相关内容可以参考http://www.albahari.com/threading/part3.aspx#_Multithreaded_Timers

5.后台线程注意事项

不仅涉及到刷新界面元素的代码段会影响界面的响应速度,后台线程也会给界面响应带来很大影响。这种影响或许可以说是间接影响,它影响的是CPU的空闲时间。当后台线程编写不当,造成CPU的高负荷运转,那么很可能CPU分身乏术,不能很好的给界面线程分配处理时间片,那么界面一样会卡的要死。

这里要说道CPU占用率的问题,它是CPU的使用时间和空闲时间的比值。当后台线程在不停止的运行时,比如死循环(while(true);),CPU占用率会直线升到90%以上。如果改为(while(true){Thread.Sleep(10);})那么CPU的占用率就降下来了。

所以后台线程在不停歇的while循环中一定要有Thread.Sleep(XX)的代码,哪怕是1ms。这里博主有个大胆的建议,没有实际的计算依据,使用数据发送速度的1/2-1/3之间的时间长度。长了数据来不及处理,短了占用的CPU时间片过高,影响界面响应。