深入浅出GUI线程安全(二)

 

原文地址: http://twaver.servasoft.com/?p=562

 

上一篇之后应该以后可以少解释为什么要考虑线程安全的了,这篇的重点是如何保证GUI线程安全。

电信网管里最常见的场景莫过于后来来了个告警需要更新界面网元,很多TWaver客户得到后台来的告警信息后很自然的去调用DataBox里Element的AlarmState,或者通过Alarm对象驱动AlarmModel了,这时问题就来了,不管你用的JMS,COBOL,MQ或者是自己起的Socket连接给你的message的thread绝对不是EDT的线程中,所以如果你直接在得到消息后更新UIModel或者UI就是违背了GUI的线程安全原则。

这种情况下你唯一能做的就是将你要做的任务注册到EDT的事件派发队列里面,如果用Swing可以调用SwingUtilities.invokeAndWait或invokeLater,用SWT可以调用Display.asyncExec或syncExec,用Silverlight和WPF可以调用Dispatcher.BeginInvoke,这样通过注册Runnable或者Action之类的任务,等待着EDT在它有空处理你的时候就会调用你的注册的Runnable或Action执行动作了,这时候在里面就是这个唯一的UI Thread在运行了,这里面你大可放心的去操作UI或绑定UI的Model,唯一需要注意的是UI Thread全局就一个在工作,当你在执行时用户界面是不会得到任何响应的,因此你必须快速处理别站着茅坑***,假设你处理了半分钟,那对用户来说他会告诉你死机了半分钟。(有人留意到我怎么没提到Flex和JavaScript的调用函数,这两个UI平台比较特殊,下篇我再细讲)。

TWaver的Demo的有很多地方都有模拟实时更新的应用,大家可以参考一下
深入浅出GUI线程安全(二)

除了调用上面提到的一些比较常规的invoke方式外你还可以考虑用Timer,SWT里面你可以调用Display.timerExec,Swing里面javax.swing.Timer,注意我这里指的不是java.util.Timer,用util的Timer那基本我们前面说的做的都白费了,javax.swing包下的那个Timer才能保证回调时的Thread是EDT,同样.NET下有起码四五个叫Timer的兄弟,System.Windows.Threading.DispatcherTimer这个我比较常用,我估计笨到能去调用System.Web.UI.Timer的人估计没兴趣在看我这枯草的全是代码的文章吧,如果你想深入了解微软问什么搞了这么多个Timer来折磨程序员的话你可以读读这篇这篇,这里不得的赞叹微软一下,大家都这家伙够啥都能搞成恐龙级,不过这家伙养的那只恐龙都有浩如烟海的详尽文档将每个细胞描述的滴水不漏,而且几乎不见文档语法带个错字的,这点很少有公司能做的足够好的,即使是google更不用说adobe了,不过文档做不到完善也无妨,只要产品质量过硬也无伤大雅,所以大家要是看到我文章的错别字还请海涵,kao,扯远了回到正题吧。

刚才上面还提到一个注意点就是在EDT里面不要执行太长时间否则用户体验很差,这点往往就是大部分不熟悉传统GUI开发者的错误偏见:CS很重很慢BS很轻量是趋势。产生这种偏见的根源我觉得是这样的:不管开发desktop还是web甚至是逐渐流行的mobile和tablet应用程序,要达到良好的用户体验都需要深入了解你所使用的GUI平台技术,但在目前这种浮躁的、项目型的、短期利益驱动的时代,你很难想象可以为一个项目中标后立刻在短期内扩展几十上百号人,或者租借大批今天做你们公司项目明天做其他公司项目的人员,而且几乎都是既懂得AIX下Oracle的tuning,又能充当美工ps两下,甚至对项目管理工具游刃有余的可以临时充当PM的万精油类型。

这些年我支持了可以说上百家的客户的确有见不仅可以“万”而且可以“精”者,但这毕竟是少数大部分都是一知半解的就开始冲锋上阵了,因此固然非常容易很多没有经验的程序员在EDT里面做一大堆工作,甚至是通讯查询数据库的工作都搞在里面了,如果能不慢那才是奇迹了,而Web的程序员里面没经验的比例其实和desktop类型的是基本一样的(争辩这个好比争辩男人聪明还是女人聪明,其实据统计学看绝顶聪明的、一般的已经弱智的比例基本是一样的),只不过Web的应用大部分获取数据在后台直接进行了,推到前台的基本就是HTML的呈现UI信息了,所以是慢在后台了用户看不见而已,用户打开一个页面很慢他是会说服务器很慢啊,他绝对不会说浏览器的HTML渲染解析引擎好慢。俺以前也经历过个庞大的项目,以前用PB做客户端,忘了是SqlServer97还是2000,跑在多年前很普通的Windows服务器上,就这么样配置架构的系统很好的能快速响应的服务于一个城市多年,后来我们这些家伙就来了,换成了HP小机(一款新出的机型,转个JDK都得到他们官方网站专门定做的JDK才能装的上,后来遇到不知道多少的咨询尝试我们才活了下来),竟然采用了Oracle的J2EE容器(六七年前大家只知道WebLogic、WebSphere和JBoss,一个三四十号人的J2EE开发团队没人见过Oracle的J2EE容器是个什么样的家伙,后来果不其然这玩意儿没少折腾我们),说实话这个系统如果按当初老系统的服务器配置根本就跑不了任何应用,完全靠的是惊人的后台N多CPU,-Xmx$$G,加上不可思议的IO能力才解决了问题,可以说是靠钱堆出了勉强可以的用户体验。

上面这个案例我只想说明不管什么应用程序要有好的应用体验一定要将数据的获取和UI的呈现逻辑分离,否则Web应用可以通过加大后台服务器配置哪怕你的SQL写的效率低点也有补救的可能(当然优化代码和SQL才是根本这个我就不争辩了),但desktop程序如果在UI Thread搞入一大堆密集预算或者长期等待的逻辑操作的话,那基本无药可救了,哪怕你让客户配置上100核的CPU也只有一个Thread在工作。

讲了这么多大道理,让我们上代码吧,我这里强烈推荐大家阅读以下TWaver的FileTreeDemo,以下两个抓图分别是TWaver Java和TWaver .NET例子的抓图
深入浅出GUI线程安全(二)深入浅出GUI线程安全(二)

public void loadChildren(final FileElement element) {
	final String oldIconURL = element.getIconURL();
	element.setIcon(loadingIcon);

	Thread t = new Thread(new Runnable(){
		public void run() {

			// fileSystemView.getFiles may cost a lot of time,
			// so we create all element in NOT swing thead.
			File file = element.getFile();
			File[] files = fileSystemView.getFiles(file, true);
			final List children = new ArrayList();
			if (files != null && files.length > 0) {
				for (int i = 0; i < files.length; ++i) {
					children.add(createElement(files[i]));
				}
			}

			// when you want to add element to the box that
			// already connected with swing component like tree,
			// network, table, sheet, etc. you should do this
			// job in swing thread.
			SwingUtilities.invokeLater(new Runnable(){
				public void run() {
					box.addElements(children, element);
					element.setLoaded(true);
					element.setIcon(oldIconURL);
					decreaseCounter();
				}
			});
		}
	});

	increaseCounter();
	t.start();
}
 

以上代码已经说明了我想说的重点,也就是当你expand目录节点时TWaver才需要你动态加载子目录数据,expend时回调的loadChildren事件就是在EDT中,而读取磁盘文件是耗时的操作,如果你直接在EDT里面查询子目录并且创建对于的element信息,肯定界面需要堵塞住知道磁盘子目录和文件读完界面才有反应,TWaver Demo的实现起了个线程去做获取数据的工作,这里注意到element其实是在普通线程创建的,这里没有问题因为其还未添加到box,还不会影响到view上,而box已经和tree绑定了,因此对box的操作必然会影响tree的更新,因此数据获并且创建成element对象之后才通过SwingUtilities.invokeLater将element添加给box,这样你才有肯呢个看到右边不断滚动的tracing信息。

面包啃完了,浓茶被我冲泡得像白开水了,先睡了明天继续