C++面试题之为什么不能多线程直接操作GUI状态

因为多线程操作一个UI,很容易导致反向加锁和死锁问题。

本文来自Sun 副总裁Graham Hamilton,
 https://community.oracle.com/hub/blogs/kgh/2004/10/19/multithreaded-toolkits-failed-dream

无法实现的梦想

我将计算机科学中的某些想法称为“Failed Dream”(出自Vernor Vinge的术语)。这种想法初步看来很好,人们隔一段时间就会重新冒出这种想法,并为此花费很多时间。通常在研究阶段,事情进展顺利,有一些让人感兴趣的成果,差不多可以应用到生产规模上了。只是总有些问题解决不了,解决了这边的问题,那边又有问题冒出来。
在我看来,多线程的 GUI 工具包,就是这种无法实现的梦想。在多线程环境中,任意一个线程都可以去更新按钮(Button)、文本字段(Text Field)等 GUI 状态,这似乎是理所当然、直截了当的做法。任意线程去更新 GUI 状态,无非是加一些锁,又有什么难的呢?实现过程中可能会有一些错误,但我们可以修复这些错误,对吧?不幸的是,实时并非如此简单。
多线程的 GUI 有种不可思议的趋势,会不断发生死锁或者条件竞争。我第一次知道这趋势,是在 80 年代初期,从 Xerox PARC 的 Cedar GUI 库中工作的那些人中听来的。这批人都十分聪明,也真正了解多线程编程。他们的 GUI 代码时不时就有死锁问题,这件事本身就很有趣。单独这事不能说明什么,或者只是特殊情况。
只是这些年来不断重复这一模式。人们最开始采用多线程,慢慢地,他们转换到了事件队列模型。“最好让事件线程做 GUI 的工作。"
我们开发 AWT 库时,也经历了类似的事情。AWT 库最初作为标准的多线程 Java 库公开。但 Java 团队回顾 AWT 的开发经验,以及人们遇到的死锁问题和条件竞争,我们开始意识到,无法履行当初做出的承诺。
事情在 1997 年的 Swing 的设计审查中有了结果,当时我们回顾了 AWT 的状况,和整个行业的经验,最终接受了 Swing 团队的建议。Swing 库应该只支持非常有限的多线程,除了某些特殊的例外情况,所有的 GUI 工具包都只在事件处理线程(主线程)上工作。其它任意线程,不应该去操作 GUI 状态。

为什么这样难?

在 1995 年,关于“事件和线程”这话题,John Ousterhout 发表了一篇很棒的 Usenix 演讲,探讨了线程驱动和事件驱动这两种编程方式的利弊。他正确地指出,为什么多线程编程会困难,为什么事件驱动更简单。我并不完全同意他对各种程序的分析,但我认同 GUI 程序的确如此。
在我看来,GUI 工具包的这种特殊的线程问题,是输入事件处理和抽象过程共同引起的。
输入事件处理的问题在于,它与多数的 GUI 活动,运行在相反的方向。GUI 操作通常开始于一个抽象的库顶层,之后从上“往下”处理。程序通过一些 GUI 对象来表达某种抽象想法。我在编写程序时,程序最开始调用高层的 GUI 抽象,之后调用较低层的 GUI 抽象,之后调用工具包内部繁琐丑陋的实现,最后去到操作系统。而输入事件却正好相反,开始于操作系统,之后逐渐“往上”分发,最终达到我的应用程序代码。
我们使用抽象来编写代码,很自然地会在每个抽象层中单独上锁。于是很不幸地,我们就遇到经典的锁定顺序噩梦:有两种不同类型的活动,按照相反的顺序,试图获取锁。因而死锁不可避免。
这个问题最开始表现为一系列特定的线程错误。人们第一反应是试图调整锁定顺序,解决特定错误。在那边释放锁,然后在这边使用更加聪明的锁定方式。这种修正很有意思,但却是徒劳的,如同对抗海洋的潮汐力。聪明的锁定方式,通常会因为缺乏锁定而导致微妙的条件竞争,或者因为巧妙和复杂的锁定,而导致巧妙和复杂的死锁。我们在 95 年到 97 年间,就经历了一堆类似的线程问题。
请注意,这种线程问题不单单发生在 GUI 工具包内部,也发生在工具包和应用程序之间。困难在于,就算整个 GUI 层的活动只采用单一的锁,在更高的层上问题也同样会出现。
那么答案是什么?要解决问题,有时你需要退后一步。观察到一个线程”向上“推送输入事件,其它线程“向下”调用抽象,这存在一个根本的矛盾冲突。就算你可以修正单独的错误,也不能修正整体的状况。
这就导致 Swing 团队所采用的解决方案,这个方案同样被大多数领先的 GUI 工具包所采用:只在单个事件线程上运行所有的 GUI 活动。这就意味着,某种意义上,所有的 GUI 活动成为了事件驱动,“向下”的线程只是一种新的事件。
这显然有效,终于可以编写可靠、复杂的 GUI 应用程序了。这值得庆贺,但是这种方法也确实使得管理长期任务更加困难。我写过一个很小的 Swing 程序。我定期使用它,从电子邮件归档中,删除一些容量大的无意义附件。我不想程序在读取几十 M 字节的电子邮件时,GUI 变得毫无反应,同时我也想显示监视器,了解删除进度。因而我编写程序时,必须小心考虑如何在工作线程处理大型任务、如何在事件线程中处理 GUI 活动,并在两者之间取得平衡。假如我有个神奇的多线程 GUI 库,类似的事情可以简单得多,事件线程模型让事情变复杂了。但事件线程有它显著的优势:工作可靠。

微妙之处

但事情如此黑白分明吗?难道就没有人成功使用多线程的 GUI 工具包?有人成功使用多线程工具包,这正是“无法实现的梦想”的一个特征:小规模可以工作,大范围无法实现。
如果多线程工具包经过精心设计,如果工具包详细暴露它的锁定细节,如果你非常聪明、非常小心,并且全面了解工具包的整个结构,满足上述的全部条件,我相信你可以成功使用多线程 GUI 工具包。但你稍微弄错某个条件,情况就会恶化,程序几乎可以正常运行,但偶然会因死锁而失去响应,或因为条件竞争而发生故障。这种多线程编程方式最适合那些密切参与工具包设计的人。
很不幸,我不认为这些限制条件可以扩展到大范围的商业用途。在商业用途中,面对的程序员普普通通,并不会十分聪明。假如某些并不明显的原因,而导致他们构建的应用程序无法可靠工作。他们就会感到沮丧和不满,并说这个工具包的坏话,尽管这个工具包是无辜的。我最开始使用 AWT 时,也说它坏话,在此道歉。
另一个多线程 GUI 成功工作的例外是:使用多个事件线程,可以在一个 Java 虚拟机上同时进行多个 GUI 活动。假如不同的 GUI 活动完全隔离,并不跟其它活动共享 GUI 组件和层次结构,并且在工具包中提供最低限度的锁定,将事件分发到正确的事件线程,这种方法是可行的。例如在一个 JVM 中运行多个 applet 小程序。但它并非通用的解决方案,绝大多数的应用程序受到约束,只能有单个事件线程。
在本文中,我一直在讨论为什么 Swing 和其它工具包本质上是单线程的。Chet 最近也发表了一些博文,讨论了相关主题。Chet 讨论了为什么多线程使得用户程序更加复杂,并且无法提升图形性能。
有些人可能会记得 "处理和监控是对称的”。是的,这是真的。从某种意义上说,我们使用事件线程来实现了一个全局锁。当然我们也可以反过来,创建一个全局锁,等价于事件队列。但这种做法是丑陋的,需要广泛的协调,并破坏了大量的抽象。但更大的问题是,除了等价于事件队列的全局锁,Java 开发人员还倾向于使用其它用途的多个锁。如果要保证这个全局锁与事件队列等价,当他们操作其它锁时,就需要遵循各种隐晦的规则。事件队列模型使得中心单独的锁更加可见和明确,帮助人们遵循模型,从而构建可靠工作的 GUI 程序。

结论

我跟大多数人一样,希望看到一个灵活、功能强大、真正的多线程 GUI 工具包。但我不知道应该如何实现它,以我经验看来,现在这些似乎很明显的多线程方法是行不通的。可能在未来几年,人们会想出一个全新的更好的方法。但现在,我们应该采用事件来编写 GUI。

翻译来自多线程 GUI 工具包:无法实现的梦想?(翻译) - 知乎 

The question came up recently of "should we make Swing truly multithreaded?" My personal answer would be "no", and here's why...

The Failed Dream

There are certain ideas in Computer Science that I think of as the "Failed Dreams" (borrowing a term from Vernor Vinge). The Failed Dreams seem like obvious good ideas. So they get periodically reinvented, and people put a lot of time and thought into them. They typically work well on a research scale and they have the intriguing attribute of almost working on a production scale. Except you can never quite get all the kinks ironed out...

For me, multithreaded GUI toolkits seem to be one of the Failed Dreams. It seems like the obvious right thing to do in a multithreaded environment. Any random thread should be able to update the GUI state of buttons, text fields, etc, etc. Damned straight. It's just a matter of having a few locks, what can be so hard? OK, there are some bugs, but we can fix them, right? Unfortunately it turns out not to be so simple...

From observation, there seems to be an amazing tendency towards deadlocks and race conditions in multithreaded GUIs. I first heard about this issue anecdotally from people who had worked with the Cedar GUI libraries at Xerox PARC in the early 80's. That was a community of extremely smart people who really understood threading, so the assertion that they were having regular deadlock issues within GUI code was intriguing. But maybe that was flawed data or an exceptional situation.

Unfortunately that general pattern has been repeated regularly down the years. People often start off trying for multithreading and then slowly move to an event queue model. "It's best to let the event thread do the GUI work."

We went through this with AWT. AWT was initially exposed as a normal multi-threaded Java library. But as the Java team looked at the experience with AWT and with the deadlocks and races that people had encountered, we began to realize that we were making a promise we couldn't keep.

This analysis culminated in one of the design reviews for Swing in 1997, when we reviewed the state of play in AWT, and the overall industry experience, and we accepted the Swing team's recommendation that Swing should support only very limited multi-threading. With a few narrow exceptions all GUI toolkit work should occur on the event processing thread. Random threads should not try to directly manipulate the GUI state.

Why is this so hard?

John Ousterhout gave a great Usenix talk on Events versus Threads in 1995 that explores some of the tradeoffs between thread-driven and event-driven programming and he correctly points out many reasons why multi-threaded programming is hard and why event driven programming can be simpler. I don't necessarily agree with his analysis for all kinds of programs, but I do agree for GUI programs.

The particular threading problems of GUI toolkits seem to me to arise from the combination of input event processing and abstraction.

The problem of input event processing is that it tends to run in the opposite direction to most GUI activity. In general, GUI operations start at the top of a stack of library abstractions and go "down". I am operating on an abstract idea in my application that is expressed by some GUI objects, so I start off in my application and call into high-level GUI abstractions, that call into lower level GUI abstractions, that call into the ugly guts of the toolkit, and thence into the OS. In contrast, input events start of at the OS layer and are progressively dispatched "up" the abstraction layers, until they arrive in my application code.

Now, since we are using abstractions, we will naturally be doing locking separately within each abstraction. And unfortunately we have the classic lock ordering nightmare: we have two different kinds of activities going on that want to acquire locks in opposite orders. So deadlock is almost inevitable.

This problem will initially surface as a series of specific threading bugs. And people's first reaction is to try to adjust the locking behavior to resolve the specific bugs. Let's release that lock there and then lets use more clever locking over here. Well, that is kind of a fun activity, but it is trying to fight back an oceanic tidal force. The cleverer locking typically turns into a combination of subtle races (due to lack of locking) or clever and intricate deadlocks (due to the clever and intricate locking). We went through a bunch of that in 95-97.

Notice that the problems extends beyond the GUI toolkit layers and also appears between the toolkit layer and the application level. With great difficult one might try to adopt a single lock for all activity within the GUI layer, but the same problem then resurfaces a level up.

So what's the answer? Well, at some point you have to step back and observe that there is a fundamental conflict here between a thread wanting to go "up" and other threads wanting to go "down", and while you can fix individual point bugs, you can't fix the overall situation.

This lead to the solution that the Swing team adopted and which is used by most leading GUI toolkits: run all GUI activity on a single event thread. This means that in some sense all GUI activity becomes event driven, and the "down" threads become just a new kind of event.

This demonstrably works. It is possible to write complex GUI apps that work reliably. Hurrah! But it does make managing long running activities tougher. I wrote a smallish Swing program that I use periodically to selectively zap large boring attachments from my email archives. I don't want to hang the GUI while it reads tens of megabytes of emails, and I also want to display a progress monitor, so I ended up having to carefully balance handing off big activities to worker threads and handing GUI activities back to the event thread. It is probably more complicated than it would be if I had a magic multi-threaded library, but it has the significant saving grace that it actually seems to work reliably.

Subtleties

Are things really so black and white? Surely there have been people who have used multi-threaded toolkits successfully? Yes, but I think this demonstrates one of the characteristics of the Failed Dreams.

I believe you can program successfully with multi-threaded GUI toolkits if the toolkit is very carefully designed; if the toolkit exposes its locking methodology in gory detail; if you are very smart, very careful, and have a global understanding of the whole structure of the toolkit. If you get one of these things slightly wrong, things will mostly work, but you will get occasional hangs (due to deadlocks) or glitches (due to races). This multithreaded approach works best for people who have been intimately involved in the design of the toolkit.

Unfortunately I don't think this set of characteristics scale to widespread commercial use. What you tend to end up with is normal smart programmers building apps that don't quite work reliably for reasons that are not at all obvious. So the authors get very disgruntled and frustrated and use bad words on the poor innocent toolkit. (Like me when I first started using AWT. Sorry!)

Another wrinkle: it is possible to have multiple simultaneous GUI activities within a Java VM by using multiple event threads. That works provided the different activities are almost entirely isolated, have their own distinct GUIs (no shared components or mixed hierarchies) and provided the very lowest toolkit level can correctly dispatch events to the right event thread with minimal locking. This is useful in (for example) running multiple applets within one JVM. But it isn't a very general solution - most applications need to live within the constraint of only a single event thread.

In this note I've most been covering why Swing and other toolkits are essentially single-threaded. Chet recently blogged on some related topics around why multi-threading complicates user programs and normally won't help raw graphics performance.

Also, before I forget, some people are probably remembering that "processes and monitors are duals". Well, yes, it's true. In some sense we are using the event thread to implement a global lock. We could invert things, and create a global lock that is equivalent to the event queue. This would be fairly ugly and would require wide coordination and undermine a lot of abstractions. But the larger problem is that Java developers tend to use multiple locks and if they are to preserve the equivalence with an event queue model, they will need to follow various non-obvious rules about how they interact with these other locks. The event queue model makes the central single lock much more visible and explicit, and on the whole that seems to help people to more reliably follow the model and thus construct GUI programs that work reliably.

Conclusion

I guess the bottom line is that like many others I would really like to see a flexible, powerful, truly multi-threaded GUI toolkit. But I don't know how to get there - at this point there is fairly strong experience that the obvious approaches for multi-threading don't work. Maybe in future years people will come up with a radically new and better approach, but for now the answer seems to be that events are our friends.

                                                                                           - Graham

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

草上爬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值