多线程工具包:一个失败的梦?
Multithreaded toolkits: A failed dream?
最近讨论的问题“我们是否让Swing真正的多线程[安全]呢?”我的个人回答是“否”,下面解释为什么...
The Failed Dream失败的梦
在《Computer Science》里,有一些自认为跟“Failed Dreams”(从 Vernor Vinge 弗诺·文奇(”此人大牛“by译者) 借来的名词)类似的概念。The Failed Dreams看起来显然是好想法。它们被周期性地改造,人们投入大量时间思考,深陷其中。他们通常在实研发阶段表现得很好,在几乎所有的商业化阶段的工作中,他们表现出有趣的特质:永远无法安静,除非把扭结解开。(意思是:实验阶段是完美的,到了实际使用的时候常出现不可捉摸的问题)
对于我来说,多线程GUI工具包是一个Failed Dream。在多线程的环境中,这显然是正确的。任何随机线程应该能够更新按钮,文本等组件的GUI状态。TMD。这只是几个锁得事情,怎么这么困难。确实!这里是有一些bug,不过,我们可以修复它呀,是吧?不幸的是,事实上这并不简单...
通过观察,在多线程GUIs中,似乎总有一个趋向于死锁和竞争状态的惊人趋势。我第一次听到有关这个问题的闲谈是在八十年代早期,是一个在Xerox PARC从事Cedar GUI libraries工作的人告诉我的。有一个社区,里面都是些极其聪明而且真正理解threading的人,他们的GUI代码定期产生死锁,这真是耐人寻味,或许是错误数据或异常状态导致的吧。
不幸的是,一般的模式已经不停重复了多年。人们不停地对多线程的进行尝试,慢慢的发展到一个事件队列模型。“事件队列模型最好地让事件线程完成GUI工作”
我们经历了AWT,AWT最初展现为一个标准的多线程java库。可是随着java研究小组观察AWT的使用经验及人们遇到的死锁和竞争,我们开始意识到,我们所做的承诺不能兑现。
这种分析在1997年Swing的设计审查中有了结果,当我们回顾AWT在整个行业历史中的状况,我们接受了Swing团队的建议:Swing应该仅支持非常有限的多线程。除了极少数例外,所有的GUI工具都应出现在事件处理进程中。随即进程不应该尝试直接操纵GUI状态。
Why is this so hard
1995年,John Ousterhout在Usenix讨论会上给出了一个重要的演讲《Events versus Threads》,探讨了线程驱动编程(thread-driven)和事件驱动编程(event-driven)之间的权衡,同时,他确切地给出很多有关为什么多线程编程很难以及为什么事件驱动编程会更容易的原因。我不一定同意他的分析适用于各种类型的程序,不过在GUI程序方面,我是非常同意的。
GUI特有的线程问题貌似由于 input event processing(输入事件处理) 和 abstraction(抽象过程) 的联合。
(这里的抽象应该是说GUI组件有容器-父组件-子组件之类的吧)
输入事件处理的问题是,事件处理通常与多数”GUI动作“在不同的方向上进行。一般来讲,”GUI动作“从一个抽象库的堆栈的顶部往下走(我的理解是:父组件响应,然后子组件响应一直……)。在我的应用程序中运作一种抽象思想,这是通过GUI objects来表达的。在应用程序中开始,通知高层的GUI抽象,它又将通知底层的GUI抽象,接着通知工具包本身,这时候进入了OS。 相反, 输入事件从OS层开始,然后再抽象堆栈中往上指派,知道进入应用程序代码(工具包)。
(英文很好懂的,翻成中文就囧了)
现在,既然我们使用abstractions,自然要对每个抽象[层]上锁。不幸的是,我们遇到了经典的锁顺序难题:我们有两种不同的动作路径,需要在相反的顺序上锁。这样死锁就无可避免了。
(上面两段文字不严谨的理解:GUI向用户的响应是从GUI最高层跑到OS层,而用户的输入事件(点击,敲键盘)则是从OS层进入GUI层,两者行动路径不同,上锁顺序相反,造成死锁)
这个问题最初表现出了一系列具体bug,而人们的第一反应就是尝试通过调整上锁行为去解决具体的bug。就是让我们在这里释放锁,然后在这里使用更多“聪明的”锁。 好吧,这是种“有趣的”动作,然而,这是在尝试抵抗海洋潮汐力。“聪明地”上锁,通常转变成难以捉摸的亦或是聪明的竞争(由于未获得锁)和 复杂的死锁(由于聪明而复杂的上锁)。95-97期间,我们经历了这一串问题。
注意,这些问题超出了GUI toolkit 层, 出现在 toolkit层(我理解为java写好的)和 application 层(自己写的代码)之间。如此困难,一个可能的尝试是:在GUI层上,对于所有的动作采用单一的锁,但是这会出现同样的问题。
那,答案是什么?好的,在某些时候你需要退后观察,这里有一个最根本的矛盾发生在一个进程是“往上”跑还是”往下“跑,如果你能解决这些独特的bug,你还不能解决大局么?
这导致了一种Swing团队采纳的解决方案,也是大多数先进的GUI toolkits所采用的:在单一事件现场上进行所有GUI动作。这意味着,在某些场景,所有的GUI动作都变成事件驱动,”往下“的线程只是一种新的事件而已。
这当然是可行的。这使得编写复杂可靠的GUI程序容易了。信春哥得永生!但是,这使得管理一个长时运作的活动更难了。我写过一个小Swing程序,我周期性的有选择地删除邮件文档中无聊的大附件。但是我不想在阅读10M字节的邮件的时候挂起GUI,然而我也想展示一个进度监视器。所以,我不得不小心翼翼地把大任务传递给worker线程,然后把GUI动作返还给event线程。这比起拥有一个不可思议的多线程库来得复杂,但是它具有重要的可取之处,这就是他实际上貌似可靠地工作着。
Subtleties(微妙、奥秘之类的意思)
事情的这么清楚了么?是否有人成功地使用多线程工具包呢?答案是肯定的,但是我认为论证演示了Failed Dreams的特征之一。
我相信你可以成功的使用多线程工具包编程,假如你的工具包设计的非常细致;假如你的工具包非常经清楚的展露它的上锁思想;假如你非常聪明细心并且对整个工具包的架构非常理解。若是以上某方面出了一点点问题,或许很可能运作了,但是
可能会偶然的挂起(由于死锁)或者失灵(由于竞争)。这种多线程方法适合一些对工具包设计非常熟悉的人。
不幸的是,我不认为这种特性能扩展到广泛的商业使用。最后只是以悲剧告终:正常优秀的程序员构建了不能可靠运作的程序,而原因却不是很明显的。所以笔者觉得十分不满和沮丧,并对这可怜无知的工具包骂脏话。
另一个缺点:在一个java虚拟机里通过多个事件处理线程进行并发的多个GUI动作。这项工程提供的不同活动几乎是完全孤立的,各自拥有自己的GUIs(没有共享组件或者混合层次),提供最小且的工具包,利用最小程度的上锁就可以正确的指派事件到正确的事件线程。这是很有用的,例如,在一个虚拟机中跑多个applets。当然这不是一个很普遍的解决方案-大多数应用程序约束在”只有单一事件线程的“条件里。
在这份说明中,大部分在述说为什么Swing和其他toolkits基本上都是单线程的。(我也?)Chet最近在博客中发飙了围绕”why multi-threading complicates user programs “
此外,有些人可能记得”处理和监视是对偶的“。当然,这是正确的。在某种意义上来说,我们使用一种事件线程实现一个全局锁。这会是”不愉快的“公平,而且需要广泛的协调和破坏大量的abstractions。但是最大的问题是,java程序员倾向于使用多个锁,如果需要他们保持与事件队列模型的等价性,他们将需要按照各种晦涩的,关于如何与这些锁交互的规则。事件队列模型central single lock更明显和明确,总体上似乎帮助人们更可靠地遵循模型,从而构造可靠地GUI程序。
Conclusion
像大多数人一样,估计文章最后是一个灵活,强大,真正的多线程GUI toolkit。不过我不知道如何搞到它——在这点上,有相当给力的经验,这就是:多线程途径明显不给力。也许在未来几年人们会拿出一种全新的更好途径,但是现在答案似乎是:events are our friends.
- by Graham
我说:”真bi唉,人家04年的文章还拿来翻译,悲催!!“