C#的WPF中使用多线程导致界面假死问题的解决

某项目需要将实时传来的渔船数据进行数据可视化,我负责Windows客户端的卡顿优化,此处的卡顿指界面无响应。

第一步是对客户端的行为的观察,观察卡顿发生的条件以及是否有规律。经过观察,客户端在网络良好的情况下卡顿4~6秒,网络较差的情况下更长,得出结论①卡顿与网络状况有关。在网络稳定的情况下观察卡顿发生的时间间隔,发现从开始卡顿到下一次开始卡顿间隔大概是20秒,得出结论②卡顿是周期性的。通过这两个结论可以进一步假设卡顿是由于周期性的网络请求导致的,网络良好的情况下响应时间短,卡顿也短,反之也成立。找到客户端的配置文件,发现该请求的确是20秒一词,假设成立。

第二步进行抓包,进一步验证,由于本项目使用SOAP通过远程过程调用来实现请求和响应,一个service有多种方法,为了确定具体是哪个请求,抓包具体看。同样,当那个请求开始的时候,客户端开始卡顿,直到那个请求结束,客户端恢复了正常,已经可以确定就是那个请求引起的客户端卡顿。

现在假设一种比较简单的情况,客户端没有使用多线程,在UI线程中请求网络,导致界面假死。遂翻看代码,代码中明晃晃的Thread thread = new Thread...,的确使用了多线程,但有些怪怪的,大致代码如下:

Thread thread = new Thread(new ThreadStart(delegate {
	this.Dispatcher.Invoke(new Action(() => {
		// 获取控件数据并且构造参数param
		...
		// 请求网络
		responseText = WebServiceHandler.ObjectWebServer.getObject(param);
		// 解析响应数据
		Object o = JsonTools<Object>.DeserializeList(responseText);
		// 更新控件
		...
	}));
}))
thread.Start();

只能看懂一部分,创建线程,然后使用delegate委托实际上是一个匿名方法传入ThreadStart中作为Thread的参数。this.Dispatcher.Invoke部分看不懂,不过大概是将一个Action作为一个委托传给Invoke方法。

于是Google之,查到如下资料:

In WPF, only the thread that created a DispatcherObject may access that object. For example, a background thread that is spun off from the main UI thread cannot update the contents of a Button that was created on the UI thread. In order for the background thread to access the Content property of the Button, the background thread must delegate the work to the Dispatcher associated with the UI thread. This is accomplished by using either Invoke or BeginInvoke. The operation is added to the event queue of the Dispatcher at the specified DispatcherPriority. Invoke is a synchronous operation; therefore, control will not return to the calling object until after the callback returns.

只有创建DispatcherObject对象的线程才能访问之,其他线程不可访问该对象,比如UI线程创建的对象其他后台线程就不可访问。那么如果想访问该UI线程创建的控件,就必须将一个访问该控件的方法委托给创建控件的UI线程的Dispatcher去执行,才能假借UI线程之手去访问控件。this.Dispatcher.Invoke就是使用UI线程的Dispatcher调用Invoke方法执行该委托,但是这个委托中使用了比较耗时的网络请求,也就是说该请求如果在Invoke方法中,就会占用UI线程的时间片去执行,即使放在了创建的线程中。

解决的方法就是将访问控件的部分放入Invoke方法汇总,其他的耗时请求就在线程的方法中执行。然后这个问题就被轻易地解决了。

这个问题产生的原因是写代码的师兄不明白如何创建线程,就去网上找到了一个创建线程的代码,并且炒上去,然后发现这个代码并不能访问看似全局数据的控件,就又去网上找代码,发现把方法放进this.Dispathcer.Invoke中就可以访问了,以为就万事大吉了。从这个例子中收获的教训是如果不完全明白代码的细节,那么尽可能先去了解,不然如果直接炒上去,那么后续完全可能产生意想不到的Bug,而且要花费更大的精力去弄明白这个用法,然后再去解决。

posted @ 2017-07-05 22:17  積水成淵  阅读(7028)  评论(2编辑  收藏  举报