如需转载请评论或简信,并注明出处,未经允许不得转载
英文原文:Multithreaded toolkits: A failed dream?
Multithreaded toolkits: A failed dream
最近出现了一个问题:“我们应该使Swing真正成为多线程吗?” 我个人的回答是“不”,这是为什么呢?
失败的梦想(The Failed Dreams)
我将计算机科学中的某些想法称为“Failed Dreams”( 出自Vernor Vinge的术语)。"Failed Dreams"虽然没能成功,但它依然属于一种美好的设想。因此,人们会定期进行重新设计,并且为之投入了大量的时间和想法。这些想法通常在理论研究层面上运作良好,并且似乎拥有在生产环境下实施的可行性,但是总是不能把所有的”扭结“都解决掉。
对我而言,多线程的GUI似乎是失败的梦想之一。在多线程环境中,似乎很显然应该可以这么做。任何随机线程都应该能够更新按钮,文本字段等的GUI状态。这只是多加了几个锁的问题,那么有什么困难呢?好的,虽然可能会遇到一些问题,但是我们可以修复,对吧?不幸的是,事实并非如此简单。
据说,在多线程GUI中似乎伴随着大量的多线程资源竞争甚至是死锁。我最初是在80年代初从Xerox PARC(Xerox Palo Alto Research Center,施乐帕克研究中心)的GUI库工作过的人那里听说这个问题的。那是一个由一些很聪明的人组成的社区,他们真正了解线程,因此断言他们在GUI代码中经常遇到的死锁问题是十分棘手的。但这也许属于是不充分的样本数据或特殊案例。
不幸的是,多年来,这种情况已经定期重复出现。人们通常开始尝试多线程,然后慢慢转移到事件队列模型。 "It's best to let the event thread do the GUI work."
我们通过AWT(Abstract Window Toolkit,抽象窗口工具包,是Java最早的用于编写图形应用程序的开发包)进行了此操作。AWT最初是作为普通的多线程Java库公开的。但是当Java团队结合AWT的经验,以及人们遇到的死锁和多线程资源相互竞争时,我们开始意识到我们所做的承诺是无法兑现的
这一问题在1997年的一次Swing设计评审中达到了高潮,当时我们回顾了AWT的运行状况以及整个行业的经验,我们接受了Swing团队的建议,即Swing仅应支持非常有限的多线程。除少数几个例子外,所有GUI工具包的工作都应在事件处理线程(主线程)上进行。其他线程不应尝试直接更新GUI的状态。
扩展:Swing 是在AWT的基础上构建的一套新的图形界面系统,它提供了AWT 所能够提供的所有功能,并且用纯粹的Java代码对AWT 的功能进行了大幅度的扩充
为什么这么困难?
John Ousterhout 于1995年在Usenix(Unix兴趣小组)上发表了一篇有关事件与线程的精彩演讲,探讨了线程驱动和事件驱动编程之间的一些折衷,他正确指出了为什么多线程编程很难并且为什么事件驱动编程可以更简单的许多原因。我不一定同意他对各种程序的分析,但是我确实同意GUI程序。
在我看来,GUI工具包的特殊线程问题似乎是由输入事件处理和抽象的结合引起的。
处理输入事件的问题是,它往往与大多数GUI绘制的执行方向相反,GUI操作从一堆抽象库的顶部开始,然后“向下”。我的应用程序中有一个设计,这个设计需要由一些GUI操作进行展现,所以我从我的应用程序开始,调用GUI顶层方法,在调用GUI底层方法,然后进入操作系统内核。而输入事件从操作系统层开始,然后逐渐“向上”调度,直到它们到达我的应用程序代码。
现在,由于我们使用抽象,我们自然会在每个抽象中分别进行加锁。不幸的是,我们有一个经典的锁排序噩梦:我们有两种不同类型的任务想要以相反的顺序获取锁,所以死锁几乎是不可避免的。
这个问题最初会作为一系列特定的多线程使用错误出现,人们的第一反应是尝试调整锁定行为来解决特定的错误让我们把锁释放,然后在这里使用更”聪明的锁“。好吧,这是一种有趣的活动,但它试图”反击海洋潮汐力“。更”聪明“的锁定通常会变成多线程资源竞争(由于缺少锁定)或”聪明“而复杂的死锁(由于”聪明“而复杂的锁定)的组合。我们在95-97年经历了很多。
注意,这些问题超出了GUI工具包层,同时又出现在工具包层和应用程序层之间。很困难的是,人们可能会尝试对GUI层中的所有任务采用一个锁,但同样的问题随后会重新出现。
那么答案是什么?好吧,在某个时刻,你必须退后一步,观察到一个线程想要“向上”和其他线程想要“向下”之间有一个根本的冲突,虽然你可以修复个别的点错误,但你不能修复整个情况。
这就产生了Swing团队采用的解决方案,大多数主流GUI工具包都使用这个解决方案:在单个事件线程上运行所有GUI任务这意味着,在某种意义上,所有的GUI任务都变成了事件驱动,而“向下”线程只是一种新的事件。
这显然有效。可以编写可靠工作的复杂GUI应用程序,这真是太棒了!但这确实使管理长期性任务变得更加困难。我写了一个小的Swing程序,我定期使用它来有选择地从我的电子邮件档案中删除大量无聊的附件。我不想在GUI读取数千万字节的电子邮件时挂起它,我还想显示一个进度监视器,因此我不得不小心地平衡将大型任务交给工作线程和将GUI任务交还给事件线程。它可能比如果我有一个神奇的多线程库的话要复杂得多,但是它有一个显著的优势,它看起来确实靠谱地运作。
扩展:所以这也就是Android中引入Handler消息机制的原因,在子线程中执行任务,执行完后将结果发送给主线程进行更新UI的操作
一些细节
事情真的是这样非黑即白吗?肯定有人成功使用了多线程工具包不是吗?是的,是的,但我认为这证明了”Failed Dreams“的特征之一。
我相信,如果该工具包经过精心设计,则可以成功使用多线程GUI工具包进行编程。如果工具包详细地公开了其锁定方法;如果您非常聪明,非常小心,并且对工具包的整体结构有全面的了解。如果你犯了一个小错误,事情就会变得严肃起来,你会偶尔出现挂机(由于死锁)或小故障(由于多线程资源竞争)。这种多线程方法最适合于与工具包设计密切相关的人。
不幸的是,我不认为这一系列的特征能够被广泛的商业应用。最终的结果往往是普通的聪明程序员开发的应用程序由于一些根本不明显的原因无法可靠地工作。因此,作者们非常不满和沮丧,并对可怜的无辜工具包使用一些不好的词(就像我刚开始使用awt时一样。对不起!)
另一个难题是:通过使用多个事件线程,可以在一个JVM中同时有多个GUI任务。如果不同的任务几乎是完全隔离的,有自己独特的GUI(没有共享组件或混合层次结构),并且最底层的工具包可以用最少的锁将事件正确地分派到正确的事件线程,那么这种方法就行了。这在(例如)在一个JVM中运行多个applet时非常有用,但这不是一个非常通用的解决方案 — 大多数应用程序只需要生活在单线程下。
在这篇文章中,我一直在讨论为什么swing和其他工具包本质上是单线程的。Chet最近在博客中谈到了一些相关的话题,比如为什么多线程会使用户程序复杂化,而且通常对原始图形性能没有帮助。
另外,在我忘记之前,有些人可能还记得“processes and monitors are duals”,是的,这是真的。在某种意义上,我们使用事件线程来实现全局锁。我们可以反转事物,并创建一个与事件队列等效的全局锁。这将是相当丑陋的,需要广泛的协调和破坏许多抽象,但更大的问题是java开发人员倾向于使用多个锁,如果他们要保持与事件队列模型的等价性,他们将需要遵循关于如何与这些其他锁交互的各种不明确的规则。事件队列模型使 central single lock更加可见和明确,从总体上看,这似乎有助于人们更可靠地遵循该模型,从而构建可靠工作的GUI程序。
结论
我认为最重要的是,与其他许多人一样,我真的很想看到一个灵活,强大,真正的多线程GUI工具包。但是我不知道如何到达那里 — 即在这一点上,有相当丰富的经验。也许在未来几年,人们会想出一种全新的,更好的方法,但目前看来,答案是”events are our friends“。
- Graham