為什麼 Go 語言允許百萬層級的 goroutines,而 Java 只允許數千層級的 threads?

來源:互聯網
上載者:User
原文連結:[https://rcoh.me/posts/why-you-can-have-a-million-go-routines-but-only-1000-java-threads/](https://rcoh.me/posts/why-you-can-have-a-million-go-routines-but-only-1000-java-threads/)很多有過 JVM 相關語言工作經驗的程式員或許都遇到過如下問題:```[error] (run-main-0) java.lang.OutOfMemoryError: unable to create native thread:[error] java.lang.OutOfMemoryError: unable to create native thread:[error] at java.base/java.lang.Thread.start0(Native Method)[error] at java.base/java.lang.Thread.start(Thread.java:813)...[error] at java.base/java.lang.Thread.run(Thread.java:844)```額,超出 thread 限制導致記憶體溢出。在作者的筆記本的 linux 上運行,這種情況一般發生在建立了 11500 個左右的 thread 時候。但如果你用 Go 語言來做類似的嘗試,每建立一個 Goroutine ,並讓它永久的 Sleep ,你會得到一個完全不同的結果。在作者的筆記本上,在作者等待的不耐煩之前,GO語言建立了大約7千萬個 Goroutine 。為什麼我們可以建立的 Goroutines 比 thread 多這麼多呢?回答這個問題需要回到作業系統層面來進行一次愉快的探索。這不僅僅是一個學術問題---在現實世界中它也揭示了如何進行軟體設計。事實上,作者碰到過很多次軟體出現 JVM 的 Thread 達到上限的情況,要麼是因為垃圾代碼導致 Thread 泄露,要麼就是因為一些開發工程師壓根不知道 JVM 有 Thread 限制這回事。## **那麼到底什麼是 Thread ?**“Thread" 本身其實可以代表很多不同的意義。在這篇文章中,作者把它描述為一種邏輯上的 Thread。Thread 由如下內容組成:一系列按照線性順序可以執行的指令(operations);和一個邏輯上可以執行的路徑。CPUs 中的每一個 Core 在同一時刻只能真正並發執行一個 logic thread<sup>[1]</sup>。這就產生了一個結論:如果你的 threads 個數大於 CPU 的 Core 個數的話,有一部分的 Threads 就必須要暫停來讓其他 Threads 工作,直到這些 Threads 到達一定的時機時才會被恢複繼續執行。而暫停和恢複一個線程,至少需要記錄兩件事情:1. 當前執行的指令位置。亦稱為:說當前線程被暫停時,線程正在執行的程式碼;2. 還需要一個棧空間。 亦可認為:這個棧空間儲存了當前線程的狀態。一個棧包含了 local 變數也就是一些指標指向堆記憶體的變數(這個是對於 Java 來說的,對於 C/C++ 可以儲存非指標)。一個進程裡面所有的 threads 是共用一個堆記憶體的<sup>[2]</sup>。有了上面兩樣東西後,cpu 在調度 thread 的時候,就有了足夠的資訊,可以暫停一個 thread,調度其他 thread 運行,然後再將暫停 thread 恢複,從而繼續執行。這些操作對於 thread 來說通常是完全透明的。從 thread 的角度來看,它一直都在連續的運行著。thread 被取消調度這樣的行為可以被觀察的唯一辦法就是測量後續操作的時間<sup>[3]</sup>。讓我們回到最初的問題,為什麼我們可以建立那麼多的 Goroutinues 呢?## **JVM使用的是作業系統的Thread**儘管規範沒有要求所有現代的通用 JVM,在我所知道的範圍內,當前市面上所有的現代通用目的的 JVM 中的 thread 都是被設計成為了作業系統的thread。下面,我將使用“使用者空間 threads" 的概念來指代被語言來調度而不是被作業系統核心調度的 threads。作業系統層級實現的 threads 主要有如下兩點限制:首先限制了 threads 的總數量,其次對於語言層面的 thread 和作業系統層面的 thread 進行 1:1 映射的情境,沒有支援海量並發的解決方案。### **JVM 中固定的棧大小****使用作業系統層面的 thread,每一個 thread 都需要耗費靜態大量的記憶體**第二個使用作業系統層面的 thread 所帶來的問題是,每一個 thread 都需要一個固定的棧記憶體。雖然這個記憶體大小是可以配置的,但在 64 位元的 JVM 環境中,一個 thread 預設使用1MB的棧記憶體。雖然你可以將預設的棧記憶體大小改小一點,但是您會權衡記憶體使用量情況, 從而增加堆疊溢位的風險。在你的代碼中遞迴次數越大,越有可能觸發棧溢出。如果使用1MB的棧預設值,那麼建立1000個 threads ,將使用 1GB 的 RAM ,雖然 RAM 現在很便宜,但是如果要建立一億個 threads ,就需要T層級的記憶體。### **Go 語言的處理辦法:動態大小的棧**Go 語言為了避免是使用過大的棧記憶體(大部分都是未使用的)導致記憶體溢出,使用了一個非常聰明的技巧:Go 的棧大小是動態,隨著儲存的資料大小增長和收縮。這不是一件簡單微小的事情,這個特性經過了好幾個版本的反覆式開發法<sup>[4]</sup>。很多其他人的關於 Go 語言的文章中都已經做了詳細的說明,本文不打算在這裡討論內部的細節。結果就是建立的一個 Goroutine 實際只佔用 4KB 的棧空間。一個棧只佔用 4KB,1GB 的記憶體可以建立 250 萬個 Goroutine,相對於 Java 一個棧佔用 1MB 的記憶體,這的確是一個很大的提高。### 在 JVM 中內容相關的切換是很慢的**使用作業系統的 threads 的最大能力一般在萬層級,主要消耗是在環境切換的延遲。**因為 JVM 是使用作業系統的 threads ,也就是說是由作業系統核心進行 threads 的調度。作業系統本身有一個所有正在啟動並執行進程和線程的列表,同時作業系統給它們中的每一個都分配一個“公平”的使用 CPU 的時間片<sup>[5]</sup>。當核心從一個 thread 切換到另外一個時候,它其實有很多事情需要去做。新線程或進程的運行必須以世界的視角開始,它可以抽象出其他線程在同一 CPU 上啟動並執行事實。本文不想在這裡多說,但是如果你感興趣的話,可以參考[這裡](https://en.wikipedia.org/wiki/Context_switch)。(t 問題的關鍵點是內容相關的切換大概需要消耗 1-100µ 秒。這個看上去好像不是很耗時,但是在現實中每次平均切換需要消耗10µ秒,如果想讓在一秒鐘內,所有的 threads 都能被調用到,那麼 threads 在一個 core 上最多隻能有 10 萬個 threads,而事實上這些 threads 自身已經沒有任何時間去做自己的有意義的工作了。**Go 語言完全不同的處理:運行多個 Goroutines 在一個 OS thread 上**Golang 語言本身有自己的調度策略,允許多個 Goroutines 運行在一個同樣的 OS thread 上。既然 Golang 能像核心一樣運行代碼的環境切換,這樣它就能省下大量的時間來避免從使用者態切換到 ring-0 的核心態再切換回來的過程。但是這隻是表面上能看到的,事實上為 Go 語言支援 100 萬的 goroutines,Go 語言其實還做了更多更複雜的事情。即使 JVM 把 threads 帶到了使用者空間,它依然無法支援百萬層級的 threads ,想象下在你的新的系統中,在 thread 間進行切換隻需要耗費100 納秒,即使只做環境切換,有也只能使 100 萬個 threads 每秒鐘做 10 次內容相關的切換,更重要的是,你必須要讓你的 CPU 滿負荷的做這樣的事情。支援真正的高並發需要另外一種最佳化思路:當你知道這個線程能做有用的工作的時候,才去調度這個線程!如果你正在運行多線程,其實無論何時,只有少部分的線程在做有用的工作。Go 語言引入了 channel 的機制來協助這種調度機制。如果一個 goroutine 正在一個空的 channel 上等待,那麼調度器就能看到這些,並不再運行這個 goroutine 。同時 Go 語言更進了一步。它把很多個大部分時間閒置 goroutines 合并到了一個自己的作業系統線程上。這樣可以通過一個線程來調度活動的 Goroutine(這個數量小得多),而是數百萬大部分狀態處於睡眠的 goroutines 被分離出來。這種機制也有助於降低延遲。除非 Java 增加一些語言特性來支援調度可見的功能,否則支援智能調度是不可能實現的。但是你可以自己在“使用者態”構建一個運行時的調度器,來調度何時線程可以工作。其實這就是構成Akka這種數百萬 actors<sup>[6]</sup> 並發架構的基礎概念。## **結語思考**未來,會有越來越多的從作業系統層面的 thread 模型向輕量級的使用者空間層級的 threads 模型遷移發生<sup>[7]</sup>。從使用角度看,使用進階的並發特性是必須的,也是唯一的需求。這種需求其實並沒有增加過多的的複雜度。如果 Go 語言改用作業系統層級的 threads 來替代目前現有的調度和棧空間自增長的機制,其實也就是在 runtime 的程式碼封裝中減少數千行的代碼。但對於大多數的使用者案例上考慮,這是一個更好的的模式。複雜度被語言庫的作者做了很好的抽象,這樣軟體工程師就可以寫出高並發的程式了。---1. 超執行緒技術(Hyperthreading)可以成倍的高效地使用cpu的核。指令流水線(Instruction pipelineing)也可以增加CPU的並存執行的能力,然而,到目前為止,它是 O(numCores)。2. 這個觀點在某些特殊的情境下是不成立的,如果有這種情境,麻煩告知作者。3. 這其實是一種攻擊媒介。Javascript 可以檢測由鍵盤中斷引起的時間微小差異。這可以被惡意網站用來偵聽,而不是你的鍵盤中斷,而是用於他們的時間。[https://mlq.me/download/keystroke_js.pdf](https://mlq.me/download/keystroke_js.pdf) [4. Go語言起初使用的是“分段棧模型“,即棧空間是被分割到記憶體中不同的地區(譯者註:在其他語言中棧空間一般是連續的),同時使用一些非常聰明的 bookkeeping 機制進行棧追蹤。後來的版本實現為了提升效能,在一些特殊的情境下, 使用連續的棧來替代“分割棧模型”。就像調整hash表一樣,分配一個新的大的棧空間,並通過一些複雜的指標操作,把所有內容都複製到新的更大的棧空間去。5. 線程可以通過調用 nice(請參閱 man nice)來標記它們的優先順序,以擷取更多資訊來控制它們被安排調度。6. 為了能實現大規模的高並發,Actor 和 Goroutines for Scala/Java 的使用者相同。就和 Goroutines 一樣,actors 的發送器可以查看哪些 actors 在他們的郵箱中有訊息,並且只運行準備好做有用工作的 actors。實際上你可以有更多的 actors,而不是你可以擁有的常式,因為 actors 不需要堆棧。然而,這意味著如果一個 actor 沒有快速處理訊息,調度器將被阻塞(因為 Actor 不具有它自己的堆棧,所以它不能在訊息中間暫停)。阻塞的調度器意味著沒有訊息處理,事情會迅速停止。這是一種折衷的處理方案。7. 在 Apache 的 網頁伺服器上,每處理一個請求就需要一個 OS 層級的 Thread,所以一個 Apache 的 網頁伺服器的並發串連效能只有數千層級。Nginx 選擇了另一種模型,即使用一個作業系統層級的 Thread 來處理成百甚至上千個並發串連,允許了更好程度的並發。Erlang 也使用了類似的模型,允許數百萬個 actors 同時運行。Gevent 將 Python 的 greenlet(使用者空間線程)帶入 Python,從而實現比其他方式支援的更高程度的並發性(Python 線程是 OS 線程)。

via: https://rcoh.me/posts/why-you-can-have-a-million-go-routines-but-only-1000-java-threads/

作者:Russell Cohen 譯者:skyismine2010 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

854 次點擊  
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.