Ractor 下多線程 Ruby 程序指南

2021-01-21 代碼混音師
什麼是 Ractor?

Ractor 是 Ruby 3 新引入的特性。Ractor 顧名思義是 Ruby 和 Actor 的組合詞。Actor 模型是一個基於通訊的、非鎖同步的並發模型。基於 Actor 的並發模型在 Ruby 中有很多應用,比如 concurrent-ruby 中的 Concurrent::Actor。Concurrent Ruby 雖然引入了大量的抽象模型,允許開發高並發的應用,但是它並不能擺脫 Ruby 的 GIL (Global Interpreter Lock),這使得同一時間,只有一個線程是活躍的。所以通常 concurrent-ruby 需要搭配無鎖的 JRuby 解釋器使用。然而,直接解除 GIL 鎖會導致大量默認 GIL 可用的依賴出現問題,在多線程開發中會產生難以預料的線程競爭問題。

去年在 RubyConf China 的時候,我問 matz 說 90 年代多核的小型機以及超級計算機已經變得非常普遍了,為什麼會把 Ruby 的多線程設計成這樣呢?matz 表示,他當時還在用裝著 Windows 95 的 PC,如果他知道以後多核會那麼普遍,他也不會把 Ruby 設計成這樣。

什麼數據可以在 Ractor 間共享?

但是,歷史遺留問題依然需要解決。隨著 Fiber Scheduler 在 Ruby 3 引用來提高 I/O 密集場景下單一線程利用率極低的問題;我們需要進一步解決,計算密集場景下,多線程的利用率。

為了解決這一問題,Ruby 3 引入了 Ractor 模型。Ractor 本質來說還是 Thread 線程,但是 Ractor 做了一系列的限制。首先,鎖是不會在 Ractor 之間共享的;也就是說,不可能有兩個線程爭搶同一個鎖。Ractor 和 Ractor 之間可以傳遞消息。Ractor 內部具有全局鎖,確保 Ractor 內的行為和原先 Thread 是一致的。傳遞消息必須是值類型的,這意味著不會有指針跨 Ractor 生存,也會避免數據競爭問題。簡而言之,Ractor 把每個 Thread 當作一個 Actor。

但 Ruby 沒有真正的值類型。但值類型的本質就是用拷貝來替代引用。我們要做的就是確保 Ruby 對象的可拷貝性。我們查看 Ractor 的文檔,我們可以看到這個的嚴格描述:

Ractors don't share everything, unlike threads.

* Most objects are *Unshareable objects*, so you don't need to care about thread-safety problem which is caused by sharing.
* Some objects are *Shareable objects*.
  * Immutable objects: frozen objects which don't refer to unshareable-objects.
    * `i = 123`: `i` is an immutable object.
    * `s = "str".freeze`: `s` is an immutable object.
    * `a = [1, [2], 3].freeze`: `a` is not an immutable object because `a` refer unshareable-object `[2]` (which is not frozen).
  * Class/Module objects
  * Special shareable objects
    * Ractor object itself.
    * And more...

Ractor 性能提升測試

為了測試出 Ractor 的效果,我們需要一個計算密集的場景。最計算密集的場景,當然就是做數學計算本身。比如我們有下面一個程序:

DAT = (0...72072000).to_a
p DAT.map { |a| a**2 }.reduce(:+)

這個程序計算 0 到 72072000 的平方和。我們運行一下這個程序,得到運行時間是 8.17s。

如果我們用傳統的多線程來寫,我們可以把程序寫成這樣:

THREADS = 8
LCM = 72072000
t = []

res = []
(0...THREADS).each do |i|
  r = Thread.new do
    dat = (((LCM/THREADS)*i)...((LCM/THREADS)*(i+1))).to_a
    res << dat.map{ |a| a ** 2 }.reduce(:+)
  end
  t << r
end

t.each { |t| t.join }
p res.reduce(:+)

運行後,我們發現,雖然確實創建了 8 個系統線程,但是總運行時間變成了 8.21s。沒有顯著的性能提升。

使用 Ractor 重寫程序,主要需要改變我們子線程內需要訪問外面的 i 變量,我們用消息的方法傳遞進去,改進後的代碼會變成這樣:

THREADS = 8
LCM = 72072000
t = []

(0...THREADS).each do |i|
  r = Ractor.new i do |j|
    dat = (((LCM/THREADS)*j)...((LCM/THREADS)*(j+1))).to_a
    dat.map{ |a| a ** 2 }.reduce(:+)
  end
  t << r
end

p t.map { |t| t.take }.reduce(:+)

其結果如何呢?我們根據不同的線程數量進行了測試。

ThreadsAMD Ryzen 7 2700xIntel i7-6820HQ18.17112.02724.4836.91334.8746.75542.3536.18852.4295.15462.2595.32071.9085.36882.1565.75492.136
103.159
112.577
122.679
132.787
142.615
152.197
162.303

Ractor 確實改善了多線程全局解釋鎖的問題。

顯微鏡下的 Ractor

我使用了 AMD uProf(對於 Intel CPU,可以使用 Intel VTune)進行 CPU 運算情況的統計。為了降低睿頻對單線程性能的影響,我將 AMD Ryzen 7 2700x 全核心鎖死 4.2GHz。

對於 AMD Ryzen 7 2700x,4 線程比單一線程快了 3 倍多。到 4 線程,比單一線程快了約 4 倍。AMD Ryzen 7 2700x 是一款 8 核心 16 線程的 CPU。同時,每 4 個核心組成一個 CCX,跨 CCX 的內存訪問有額外的代價。這使得 4 線程內性能提升很顯著,超過 4 線程後受限於 CCX 和 SMT,性能提升變得比較有限。其表現是隨著線程數的增加,IPC(每時鐘周期指令數)開始下降。在單線程運算時,每時鐘周期 CPU 可以執行 2.42 個指令;但到了 16 線程運算時,每時鐘周期 CPU 只能執行 1.40 個指令。同時,更多的線程意味著更複雜的作業系統的線程調度,使得多核的利用率越來越低。

同樣,對於 Intel i7-6820HQ,我們得到了類似的結論。這是一款 4 核 8 線程的 CPU,由於第 5 個線程開始需要使用 HT,從而提升變得很有限。

Ractor 如何改善現有 Ruby 程序的性能?

Ractor 的引入除了可以改善計算密集場景下的運算效率,對於現有大型 Ruby Web 程序的內存佔用也是有積極意義的。現有 Web 伺服器,比如 puma,由於 I/O 多路復用性能極其低下,通常會使用多線程 + 多進程的形式來提升性能。由於 Web 伺服器可以自由水平擴展,使用多進程的形式來管理,可以完全解開 GIL 鎖的問題。

但是 fork 指令效率低下。微軟在 2019 年 HOTOS 上給出了一篇論文:A fork() in the road,和 spawn 相比,fork 模式會導致啟動速度變得非常慢。為了緩解這一問題,在 Ruby 2.7 引入 GC.compact 後,通常需要執行多次 compact 來降低 fork 啟動的消耗。進一步地,使用 Ractor 來替代多進程管理,可以更容易地傳遞消息,復用可凍結的常量,從而降低內存佔用。

總結

Ruby 3 打開了多線程的潘多拉盒子。我們可以更好利用多線程來改善性能。但是看著 CPU Profiler 下不同線程調用會導致 CPU IPC 下降和緩存命中下降,對程序調優也提出了更高的要求。

我們邊走邊看吧。

相關焦點

  • Ruby 3.0發布,比 Ruby2快3倍
    RBSRBS 是一種描述 Ruby 程序類型的語言。類型檢查器(包括類型分析器和其他支持 RBS 的工具)將通過 RBS 定義更好地理解 Ruby 程序。開發者可以寫下類和模塊的定義:類中定義的方法、實例變量及其類型以及繼承/混合關係。
  • Ruby 3發布,為何性能能提升3倍
    但是在版本3中,它是全自動的,適當調用壓縮程序以確保適當的內存利用率。對象分組  垃圾壓縮器移動堆中的對象。它將分散的對象組合在一起放在內存中的某個位置,以便後面更大的對象可以有效利用內存。2. Ruby 3中的並行性和並發性  並發是任何程式語言的重要關注點之一。Matz認為Ruby程式設計師未能正確地使用線程這一抽象層。
  • 看完這5本Ruby書目,目標找到Ruby工作
    Ruby程序設計語言K語簡介:「《Ruby程序設計語言》是Ruby的權威指南,涵蓋了該語言的1.8和1.9版本。        《Ruby程序設計語言》不僅詳細說明了語言規範。它適用於第一次接觸ruby的高級程式設計師,以及那些想挑戰自己對ruby語言理解並想更深入掌握它的人。
  • 通過開源書籍學習 Ruby 編程
    使用 mocks 和 stubs◈ 通過利用 Ruby 神秘的力量來設計漂亮的 API:靈活的參數處理和代碼塊◈ 利用動態工具包向開發者展示如何構建靈活的界面,實現單對象行為,擴展和修改已有代碼,以及程序化地構建類和模塊◈ 文本處理和文件管理集中於正則表達式,文件、臨時文件標準庫以及文本處理策略實戰◈ 函數式編程技術優化了模塊代碼組織、存儲、無窮目錄以及更高順序程序。
  • Ruby CGI 編程
    使用Ruby您不僅可以編寫自己的SMTP伺服器,FTP程序,或Ruby Web伺服器,而且還可以使用Ruby進行CGI編程。接下來,讓我們花點時間來學校Ruby的CGI編輯。CGI程序可以是 Ruby 腳本,Python 腳本,PERL 腳本,SHELL 腳本,C 或者 C++ 程序等。CGI架構圖Web伺服器支持及配置在你進行CGI編程前,確保您的Web伺服器支持CGI及已經配置了CGI的處理程序。
  • 數學極差的程式設計師-ruby之父
    那時候的他對彙編和Basic都不感興趣,他想自己創造一門語言,當時的他連語言的名字都想好了,可是後來記載著他的程式語言的筆記被他弄丟了,他也只好作罷,不過要做一門程式語言的種子已經在他的心底種下了。進入大學裡面,他變成了一個宅男,每天做的最多的事情就是看書,偶爾會看看電影。他很少運動,也許那時候的他已經具備了做一個程式設計師的基本素質,那就是宅。
  • Java項目實踐,CountDownLatch實現多線程閉鎖
    摘要本文主要介紹Java多線程並發中閉鎖(Latch)的基本概念、原理、示例代碼、應用場景,通過學習,可以掌握多線程並發時閉鎖(Latch)的使用方法。概念「閉鎖」就是指一個被鎖住了的門將線程a擋在了門外(等待執行),只有當門打開後(其他線程執行完畢),門上的鎖才會被打開,a才能夠繼續執行。
  • Scratch克隆技術、多線程編程及通訊技術初探
    但另一方面,多線程編程及其同步技術這種「高上限」又無法迴避——這是開發複雜應用程式最實用但又最複雜的技術之一。還是那句話,Scratch絕不是玩具式語言!下面通過實例來說明問題。二、問題需求在本文中,我們想使用Scratch開發一個如圖所示的小程序。
  • Ruby 語言教程
    Ruby 是一種開源的面向對象程序設計的伺服器端腳本語言,在 20 世紀 90 年代中期由日本的松本行弘(まつもとゆきひろ/Yukihiro
  • Ruby一行式命令總結和常用技巧
    $ printf 'gate\napple\nwhat\n' | ruby -ne 'print if /at/'$ printf 'gate\napple\nwhat\n' | ruby -ne 'print if ~/at/'$ printf 'gate\napple\nwhat\n' | ruby -ne 'print
  • RubyMiner挖礦程序24小時內影響全球30%的網絡
    義大利安全公司 Certego 也注意到 RubyMiner 從 1 月 10 日就開始發起攻擊:從昨天(1月10日)23:00開始,我們的威脅情報平臺就已經開始大規模報告關於 ruby http 的利用。令人驚訝的是,黑客大量使用 2012 年和 2013 年發布和修補的舊漏洞,而且似乎並不打算隱藏自己的蹤跡,而是打算在最短的時間內感染大量的伺服器。
  • mac口紅ruby woo多少錢?ruby woo口紅專櫃價格
    MAC ruby woo唇膏,RUBY WOO幾乎適用於任何膚色。亞光優雅紅,高顯色度,唯一的retro matte紅,就是霧面啞光,代表了更加復古更加復刻的一種態度。因為霧面的啞光感,所以ruby woo真的有點幹的,在使用之前可以用一支滋潤度比較高的唇膏打底,既保溼,也能撫平唇紋,卸妝也好卸掉。
  • 初學者的Ruby語言第3部分:Ruby字符串
    - 在Ruby中連接不同的類型讓我們嘗試連接一個數字和一個字符串:number = 10puts "Hacking Code " + number如果我們執行:ruby-strings.rb:7:in +': can't convert Fixnum into String (TypeError) from ruby-strings.rb:7:in'在Ruby中,我們必須先將其轉換。
  • 程式語言 Ruby 如何還能再活 25 年?
    這也許不是大家希望在編程大會上聽到的主題演講者所提出的第一個問題,但這是來自日本的,Ruby 程式語言的創始人,和藹可親的松本行弘(Yukihiro Matsumoto,被稱為 Matz),在為期兩天的年度 Bath Ruby 大會上,與 500 多位 Ruby 開發者交談時提出的第一個問題。
  • 優雅終止線程?系統內存佔用較高?
    測試結果StringWriterOOMTest運行時的整個進程內存大小在Windows任務管理器中達10300多MB時,程序停止。所以是否可以猜想:jvm只是向作業系統申請了這麼多內存暫時沒有歸還回去,留待下次線程池有新任務時繼續復用呢?本文最後一部分試驗就圍繞著一點展開。
  • RubyMine 4.5 發布,Ruby 集成開發環境
    圖文並茂的介紹內容請看:http://www.jetbrains.com/ruby/whatsnew/index.htmlRubyMine 是一個全新的為Ruby 和 Rails開發者準備的 IDE ,RubyMine由 JetBrains 開發(JetBrains最著名的產品之一就是Java IDE:IntellJ IDEA了!)。
  • Ruby工具和擴展的快速指南
    約定優於配置: Ruby on Rails支持它認為是構建Web應用程式的最佳方式。使用Ruby時,開發人員需要將這些實踐作為一組默認約定。這使您可以更快地部署應用程式,而不是讓團隊花時間無休止地配置文件。
  • Java必懂線程,這些你都知道麼?
    }Synchronized 橫切面詳解Jdk 1.2 之後把重量級鎖進行了升級(偏量級鎖(貼標籤,線程指針)-輕量級鎖(自旋鎖)如果多個線程產生競爭,自動升為輕量級鎖 搶lock Record ) -重量級鎖拿出來4個字節代表分代年齡(老年代。)
  • 告別性能問題:Ruby 3.0正式發布
    首先,異步線程將由 Fibers 進行控制,因為當前的伺服器會在釋放其它線程時阻塞 I/O 操作,比如 API 調用 / 資料庫操作等。(圖 via Sloboda Studio)其次是啟用基於 Fibers over Threads的線程操作,是因為這麼做能夠減少上下文的切換開銷。
  • 32 款旗艦手機處理器多線程 PK:三星 Exynos8890 稱雄
    IT之家訊 11月16日消息,繼單線程跑分對比之後,2013年至今的32款旗艦手機處理器的Geekbench3多線程跑分對比也出來了,與之前單線程跑分不同的是,蘋果在多線程不再是獨樹一幟的存在,反而不如安卓平臺。