該不該使用Reactive程式設計?先預覽一下Loom專案中的Reactive模型和協程 - frankel

banq發表於2020-06-23

Java 15將釋出Project Loom的第一個版本。我相信這將改變JVM。在本文中,我想深入探討導致我相信這一點的原因。
首先,我們需要了解核心問題。然後,我將嘗試描述以前的技術如何解決它。之後,我們將看到Project Loom採取的方法。最後,我將推斷後者可能對生態系統產生什麼影響。

執行緒
我們首先必須記住,很長一段時間以來,計算機只有一個核心。即使這樣,還是需要同時執行多個程式:這至少要執行兩個作業系統和適當的程式。
為了實現並行性的幻覺,它依賴於一個技巧。它執行一個程式,如果該程式沒有在特定的時間範圍內完成,它將儲存其狀態以供以後使用。然後,它執行下一個要執行的程式。有幾種演算法可用於排程下一個程式:輪循,加權輪循等。優點是,所有這些都可以透過作業系統執行緒的概念很好地從開發人員中抽象出來。
但是,執行緒有兩個缺點:

  • 執行緒很重,因為它帶有很多狀態
  • 執行緒需要大量的機器資源來建立

在所有情況下,現代OS都允許M個執行緒,其中M是數千個執行緒。現代機器是多核的,提供N個核,比M少兩個數量級。擁有多個核心(無論是物理核心還是虛擬核心)並不會從根本上改變底層機制:M遠高於N,並且作業系統負責從執行緒到核心的對映。

阻塞執行緒
上面的模型在傳統方案中效果很好,但在Web方案中效果不佳。假設有一個Web伺服器需要響應HTTP請求。在過去不太好的時候,CGI是處理請求的一種方法。它將每個請求對映到一個流程,以便處理建立整個新流程所需的請求,並在傳送響應後將其清除。
Java EE應用程式伺服器極大地改善了這種情況,因為實現方式將執行緒保留在池中以供以後重用。但是,假設響應的生成需要時間,例如因為需要訪問資料庫以讀取資料。在資料庫返回資料之前,執行緒需要等待。
這意味著執行緒實際上正在等待其生命週期的大部分時間。一方面,此類執行緒不自行使用任何CPU。另一方面,它使用其他種類的資源,尤其是記憶體。
同樣,太多執行緒是作業系統的負擔:作業系統必須在數量有限的CPU核心上平衡大量執行緒。這花費了寶貴的CPU週期,因此,作業系統正在與應用程式爭奪CPU。
現在,併發請求的數量可以遠遠超過伺服器可用的執行緒數量。因此,阻塞的執行緒浪費資源,並使伺服器無響應。為了解決這個問題,可以新增更多的Web伺服器來處理負載:這是水平縮放。
在大多數情況下,水平縮放就足夠了。等待阻塞執行緒所花費的時間是浪費的,但沒有相關的成本...除非一個人的基礎架構駐留在``雲''中。在那種情況下,人們要為未使用的資源付費:這絕不是一個明智的主意。

單執行緒,反應式和Kotlin協程模型
對於開發人員來說,管理多個執行緒很複雜。例如,如果您一直在使用(或開發)Swing應用程式,則可能知道“灰色矩形效果”。當使用者與視窗的互動(例如單擊)啟動長時間執行的任務時,就會發生這種情況。如果將另一個視窗移到Swing視窗上,然後再移出,Swing不會重畫另一個視窗與Swing視窗相交的區域,而會留下難看的灰色矩形。原因是長時間執行的任務是在Event Dispatch Thread上啟動的,而不是在專用執行緒上啟動的。而且這很容易避免,甚至不涉及在共享的可變狀態上進行同步!
為避免這種情況,某些堆疊完全禁止開發人員使用多個執行緒。例如,Node.js的API僅提供一個非阻塞事件迴圈執行緒:提交的函式採用回撥的形式。請注意,這不會阻止實現使用多個執行緒。
Reactive的方法是另一種選擇,其實頗為相似。儘管它擺脫了單執行緒API的限制並提供了反壓力機制,但它仍然需要非阻塞程式碼。由於OS執行緒很昂貴,因此Reactive將它們池化,並在整個應用程式生命週期中重複使用它們。核心過程是從池中獲取空閒執行緒,讓其執行程式碼,然後將執行緒釋放回池中。這就是為什麼它需要非阻塞程式碼的原因:如果程式碼阻塞了,那麼執行執行緒將不會被釋放,並且池將在某一點或另一點耗盡。
我感興趣地觀察了Reactive模型如何像篝火一樣在Spring生態系統中傳播,儘管我選擇站在一邊。恕我直言,反應式有幾個缺點:

  • 編寫(和閱讀!)反應式程式碼的思維方式與編寫傳統程式碼的思維方式非常不同。我願意承認改變心態只需要時間,持續時間取決於每個開發人員。
  • 儘管真正的開發人員不會除錯,但我知道很多人會除錯-包括我自己。由於上面的執行緒切換魔術,要跟蹤一段程式碼及其相關的狀態並不容易。這需要足夠的工具,例如帶有相關外掛的 IntelliJ IDEA 。
  • 最後,出於相同的原因,傳統的堆疊跟蹤也無濟於事。一些黑魔法可以繞開它。但是,這不是因為膽小。有關選項的完整列表,請檢視此文件

Kotlin語言提供了反應式方法的替代方法:協程。簡而言之,當使用suspend關鍵字時,Kotlin編譯器會在位元組碼中生成一個有限狀態機。好處是,在協程塊中呼叫的函式看起來像是順序執行的,儘管它們是並行執行的-更確切地說,取決於確切的範圍。

Project Loom和虛擬執行緒
Reactive模型和Kotlin協程都在客戶端程式碼和JVM執行緒之間新增了一個額外的抽象層。框架/庫的職責是動態地將一個對映到另一個。問題的癥結在於JVM執行緒是OS執行緒的薄包裝:請記住,OS執行緒建立起來很昂貴,並且數量上限制在幾千個。
Project Loom的目標是實際上將JVM執行緒與OS執行緒解耦。當我第一次意識到該計劃時,其想法是建立一個稱為Fiber(纖程,您能趕上潮流嗎?)的抽象。一個Fiber責任是讓一個作業系統執行緒,使其執行程式碼,釋放回池。
當前的建議有很大的不同:Fiber它沒有使用新的類,而是重新使用了Java開發人員非常熟悉的一個類- java.lang.Thread!因此,在新的JVM版本中,某些Thread物件可能是重量級的並對映到OS執行緒,而另一些物件可能是虛擬執行緒。

發展後果
主要問題是,既然JVM API提供了對OS執行緒的抽象,那麼其他抽象(例如響應式和協程)又會變成什麼樣呢?我對預測不滿意,但以下是Reactive /協程背後公司的一些可能態度:

  1. 結果是,他們意識到自己的框架不再帶來任何附加值,而只是重複。他們停止了開發工作,僅向現有客戶提供維護版本。他們幫助說客戶遷移到新的ThreadAPI,一些幫助可能是以付費諮詢的形式。
  2. 相反,他們在各自的框架中投入了大量精力後,決定繼續進行,好像什麼也沒有發生。例如,Spring框架負責實際設計一個共享的Reactive API,稱為Reactive Streams,沒有Spring依賴項。當前有兩種實現,RxJava v2和Pivotal的Project Reactor。在他們方面,JetBrains宣傳Kotlin的協程是並行執行程式碼的最簡單方法。
  3. 最後,可能有中間立場。這兩個框架都將繼續其生命,但是會將它們各自的基礎實現更改為使用虛擬執行緒。

由於沉沒成本的謬誤,排在第一位的可能性極小:銷售和市場營銷將努力保持其“競爭優勢”-無論在他們眼中意味著什麼。儘管有些工程師出於相同的原因希望保留現有程式碼,但其他一些工程師則將努力使用新的API。因此,我也不相信第二名也會發生。但是,我認為兩個工程派系之間都在發揮力量,然後它們與市場營銷/銷售之間將在#3之間找到平衡。

結論
Project Looms將現有的Thread實現方式從OS執行緒的對映更改為可以表示此類執行緒或虛擬執行緒的抽象。就其本身而言,這是一個有趣的舉動,它在一個平臺上歷來都比創新具有更多的向後相容性價值。與其他最新的Java版本相比,此功能是真正的遊戲規則改變者。一般而言,開發人員應儘快開始熟悉它。打算學習Reactive和協程的開發人員應該退後一步,並評估他們是否應該學習新的ThreadAPI-還是不應該。
 

相關文章