多執行緒 GUI 工具包:無法實現的夢想?(翻譯)
此文翻譯自ofollow,noindex">Multithreaded toolkits: A failed dream? , 解釋了為什麼不能多執行緒去操作 GUI 狀態。英文原文很早之前發表的,很不錯。但我翻譯得不好,翻譯只是強迫自己讀下去。
----------------------
最近有人提出一個問題,“我們是否應該讓 Swing 庫真正實現多執行緒?” 我個人覺得不應該,下面說明為什麼。
無法實現的夢想(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 庫,類似的事情可以簡單得多,事件執行緒模型使得事情變複雜了。但事件執行緒有它顯著的優勢,它的確工作得可靠。
微妙之處(Subtleties)
但事情真的如此黑白分明嗎?曾經有人成功使用多執行緒的 GUI 工具包嗎?是的,有人成功使用多執行緒工具包,這正是無法實現的夢想的特徵之一。
如果多執行緒工具包經過精心設計,如果工具包詳細暴露它的鎖定細節,如果你非常聰明、非常小心,並且全面瞭解工具包的整個結構,滿足上述的全部條件,我相信你可以成功使用多執行緒 GUI 工具包。但你稍微弄錯某個條件,情況就會發生變化,程式幾乎可以正常執行,但偶然會因死鎖而失去響應,或因為競爭條件而發生故障。這種多執行緒程式設計方式最適合那些密切參與工具包設計的人。
很不幸,我不認為這些限制條件可以擴充套件到廣泛的商業用途。在商業用途中,面對的程式設計師普普通通,並不會十分聰明。假如他們構建的應用程式由於並不明顯的原因而無法可靠工作,就會感到沮喪和不滿,並說這個工具包的壞話,儘管這個工具包是無辜的。就像我最開始使用 AWT 時那樣,在此要說聲對不起。
另一個多執行緒 GUI 成功工作的例外是:使用多個事件執行緒,可以在一個 Java 虛擬機器上同時進行多個 GUI 活動。假如不同的 GUI 活動是完全隔離的,並不跟其它活動共享 GUI 元件和層次結構,並且在工具包中提供最低限度的鎖定,將事件分發到正確的事件執行緒,這種方法是可行的。例如在一個 JVM 中執行多個 applet 小程式。但它並非通用的解決方案,絕大多數的應用程式還是需要約束,只能有單個事件執行緒。
在本文中,我一直在討論為什麼 Swing 和其它工具包本質上是單執行緒的。Chet 最近也發表了一些博文,討論了相關主題,為什麼多執行緒使得使用者程式更加複雜,並且無法提升圖形效能。
有些人可能會記得 "處理和監控是對稱的”。是的,這是真的。從某種意義上說,我們使用事件執行緒來實現了一個全域性鎖。當然我們也可以反過來,建立一個全域性鎖,等價於事件佇列。但這種做法是醜陋的,需要廣泛的協調,並破壞了大量的抽象。但更大的問題是,Java 開發人員傾向於使用多個鎖。如果他們要保證這個全域性鎖與事件佇列等價,當他們跟其它鎖互動的時候,就需要遵循各種隱晦的規則。事件佇列模型使得中心單獨的鎖更加可見和明確,幫助人們更加可信地遵循模型,從而構建可靠工作的 GUI 程式。
結論
我跟大多數人一樣,希望看到一個靈活、功能強大、真正的多執行緒 GUI 工具包。但我不知道應該如何實現它,以我經驗看來,現在這些似乎很明顯的多執行緒方法是行不通的。可能在未來幾年,人們會想出一個全新的更好的方法。但現在,我們應該採用事件來編寫 GUI。