您的位置:首页 > 产品设计 > UI/UE

为什么说GUI是单线程化的

2016-05-31 14:39 274 查看
现代的gui框架使用了一个略微不同的模型:模型创建了一个专门的线程,事件派发线程(event dispatch thread,RDT)来处理gui事件。单线程化的GUI框架并不仅仅存在于java中:Qt、NextStep、Macos Cocoa、XWindows,等等都是单线程化的。也并不缺少反面的尝试;有很多试图写出多线程的GUI框架的努力,最终都由于竞争条件和死锁导致的稳定性问题,又回到了单线程化的事件队列模型的老路上来:采用一个专门的线程从队列中抽取事件,并把它们转发给应用程序定义的事件处理器。(awt最初曾尝试在某种程度上支持多线程访问,单线程化地实现swing的决定主要基于AWT中的经验和教训。)多线程的GUI框架会尤其易受死锁的影响,部分原因在于,输入事件处理与任何GUI组件背后的对象模型之间存在偶发的交互。用户发起的动作总会冒泡似的从操作系统传递给应用程序—先是由os检测到一次鼠标点击,然后工具集把它转化为“鼠标点击”事件,最终它会作为一个高层事件(比如“buttonpressed”事件)转发给应用程序的监听器。另一方面,应用程序发起的动作又会以冒泡的形式传回操作系统—应用程序发起一个动作要改变某个组件的背景颜色,这会被转发给一个特定的组件类,最终转发给os进行渲染。两种动作以完全相反的顺序访问相同的GUI对象,需要保证让每一个对象都是线程安全的,这会导致一系列的锁顺序的不一致,这会直接引发死锁。这个问题机会在每一次GUI工具集的开发中都会出现,是经验之谈。模型—视图—控制器(mvc)模式的普遍流行形成了导致多线程GUI框架出现死锁的另一原因。把用户的互交分拨到模型、视图和控制器之间的写作中,极大地简化了GUI应用程序的实现,但这让不一致的锁顺序再次雪上加霜。控制器调用模型,模型通知视图已经发生了一些事情。控制器同样可以调用视图,视图可以依次回调模型来查询模型的状态。结果是,不一致的锁顺序再次伴随死锁的风险一同到来。Graham hamilton,sun公司的vp,在他的weblog中详尽地概括了这些挑战,描述了多线程GUI工具集之所以会成为计算机科学史上又一次“失败的梦”。如果多线程GUI工具集经过非常谨慎的设计;如果工具集能使它加锁的方法鲜明地显露;如果你非常聪明,非常仔细,并且对工具集的整体框架有着全局的把握,我相信你还是可以成功地编写出多线程的GUI来。但是如果这些事情有一些轻微的偏差,程序多数时候仍然运行良好,但是你会偶尔看到程序挂起或者运行故障。那些密切参与了工具集设计的人能够很好地运用这种多线程方案。不幸的是,我认为这些特性并没有和商业流行度成正比。一个中等能力的程序员,整日构建着被一些莫名其妙的原因困扰着而不能稳定运行的应用程序,我们很容易落入这种境界。于是应该程序的作者会倍感怨恨与失落,对无辜的工具集恶语相加。单线程化的GUI框架通过现场限制来达到现场安全性;所有GUI中的对象,包括可视组件和数据模型,都只是被事件线程访问。当然,这只把线程安全负担的一部分推给了应用程序的开发者,他们必须确保这些对象是被正确限制的。

单线程消息队列机制

首先我还是说一下我对GUI单线程消息队列机制的理解,这是我大学里几年编程经验赚来的知其然的部分。

Android、Swing、MFC等的GUI库都使用单线程消息队列机制来处理绘制界面、事件响应等消息,在这种设计中,每个待处理的任务都被封装成一个消息添加到消息队列中。消息队列是线程安全的(消息队列自己通过加锁等机制保证消息不会在多线程竞争中丢失),任何线程都可以添加消息到这个队列中,但是只有主线程(UI线程)从中取出消息,并执行消息的响应函数,这就保证了只有主线程才去执行这些操作。

单线程消息队列机制存在一个问题:消息响应函数中不能有耗时长的、计算密集型的操作,因为主线程在努力地处理这样的操作的时候就无法去处理其它的积压在消息队列中的绘制消息、事件消息了(一个消息处理完了主线程才会去队列中取下一个消息),这时候就会出现按键无响应、点击无反应的情况。

但这个问题有完美的解决方案,我们可以在消息响应函数中启动另一个工作线程(Worker Thread)来执行耗时操作,这样在线程启动起来后这个消息就算处理完了,主线程可以取下一个消息了,这时候主线程和还未执行完计算任务的工作线程就在操作系统的调度下并驾齐驱地狂奔了(调度算法会保证两个线程并发或并行地执行,不会专宠某个线程)。

一般我们在耗时任务执行完后还要更新界面展示计算的结果,由于我们不能直接在工作线程中更新界面,所以可能有些小伙伴直接在消息响应函数中线程start后就接着调用join来等待线程结束以更新界面,这其实相当于把耗时任务直接放在主线程去执行,因为在消息响应函数中join其实就是主线程在join,积压的消息是得不到处理的。正确的处理办法是将耗时任务改为异步通知机制,即工作线程向消息队列中添加消息以通知主线程耗时任务完成了,这样主线程在启动工作线程后就不需要主动地去调查任务的进展了,“任务结束的时候它会通知我的”,主线程如是说。

工作线程向主线程的消息队列添加消息的常用方法如下:

l Android:Acitvity.runOnUiThead、Handler.post、AsyncTask

l Swing:SwingUtilities.invokeLater

l Win32、MFC:自定义用户消息,在工作线程中PostMessage

GUI为什么不设计为多线程

大部分的GUI toolkits都是设计为上面的单线程消息队列机制,为什么不设计为多线程的呢?如果GUI是多线程的,CPU又是多核的话,多个CPU核心可以并行地执行绘制等操作,界面响应的速度应该是成倍提升的;而且就算是其中有多线程共享的资源加锁不就行了吗?

在google搜索的过程中我看到了负责Swing开发的一个大师的一篇博客《Multithreaded toolkits: A failed dream?》:

https://weblogs.java.net/blog/kgh/archive/2004/10/multithreaded_t.html

从中我了解到开发多线程的GUI toolkits是一件吃力不讨好的事,不仅开发难度大Bug多多,用起来也未必可以获得理想中的效果,其中的死锁和竞争,大师们也感到头疼。

多线程GUI加锁困难

为什么这么困难?大师讲了一个例子,我们通过用户级的代码去改变界面如TextView.setText走的是个自顶向下的流程:


而系统底层发起的如键盘事件、点击事件走的是个自底向上的流程:



这样就麻烦了,因为为了避免死锁,每个流程都要走一样的加锁顺序,而GUI中的这两个流程却是完全相反的,如果每一层都有一个锁的话加锁就是个难以完成的任务了,而如果每一层都共用一个锁的话,那就跟单线程没区别了。

于是GUI toolkits的开发者就“不负责任”地把GUI设计成了单线程消息队列机制,然后他们还说界面更新一般不是瓶颈,单线程足够了。然后我瞬间想到了3D游戏,单线程对于3D应该是很吃力的,但实际上负责3D绘制的是显卡的GPU,GPU不像CPU那样事无巨细、事必亲躬、鞠躬尽瘁、死而后已,只负责画好它的图就可以了,所以并行起来不是件困难的事。

Java Swing是一个单线程图形库,里面的绝大多数代码不是线程安全(thr
4000
ead-safe)的,看看Swing各个组件的API,你可以发现绝大多数没有做同步等线程安全的处理,这意味着它并不是在任何地方都能随便调用的(假如你不是在做实验的话),在不同线程里面随便使用这些API去更新界面元素如设置值,更新颜色很可能会出现问题。

虽然Swing的API不是线程安全,但是如果你按照规范写代码(这个规范后面说),Swing框架用了其他方式来保障线程安全,那就是Event Queue和EDT,我们先来看一幅图:



从上图我们可以形象的看到,在GUI界面上发出的请求事件如窗口移动,刷新,按钮点击,不管是单个的还是并发的,都会被放入事件队列(Event Queue)里面进行排队,然后事件分发线程(Event Dispatch Thread)会将它们一个一个取出,分派到相应的事件处理方法。前面我们之所以说Swing是单线程图形包就是因为处理GUI事件的事件分发线程只有一个,只要你不停止这个GUI程序,EDT就会永不间断去处理请求。

那这种“单线程队列模型”的好处是什么呢?在ITPUB的java gui的《深入浅出Swing事件分发线程》文中总结了两点:

(1)将同步操作转为异步操作

(2)将并行处理转换为串行顺序处理

所以也就难怪目前大多数GUI框架都是采用的是这种单线程的模型.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: