Ruby 的多線程
本章節(jié)讓我們來學習 Ruby 的多線程。您將會了解到:什么是多線程,Ruby 中如何創(chuàng)建線程等知識。
1. Ruby 中的線程
通俗一點來講,線程可以讓程序同時執(zhí)行多項操作。
比如:讀取多個文件、處理多個請求、建立多個API連接。多線程可以更好地利用CPU的核心,CPU的一個核好比一個普通人,一個普通人只能干一件事,多個人可以分開干不同的事或干很多次同樣的事。
注意事項:
在MRI(Matz 的 Ruby 解釋器)中,這是運行 Ruby 應用程序的默認方式,只有在運行 I/O 綁定的應用程序時,您才能從線程中受益。由于存在 GIL(Global Interpreter Lock,是由編程語言解釋器線程持有的互斥鎖,以避免與其他線程共享不是線程安全的代碼。),因此存在此限制。對于一般的 Ruby 和 Python 應用,即使在多核處理器上運行,使用 GIL 的解釋器始終總是允許一次僅執(zhí)行一個線程。
每個進程都有至少一個線程,您可以按需創(chuàng)建更多線程。
2. I/O 綁定應用程序
首先,我們需要討論 CPU 綁定和 I/O 綁定應用程序之間的區(qū)別。
I/O 綁定應用程序是需要等待外部資源的應用程序:
- API請求;
- 數(shù)據庫(查詢結果);
- 磁盤讀取。
線程可以在等待資源可用時決定停止。
這意味著另一個線程可以運行并執(zhí)行其任務,而不會浪費時間等待。
I/O 綁定應用程序的一個示例是 Web 爬蟲(crawler)。
對于每個請求,爬蟲都必須等待服務器響應,并且在等待時它什么也不能做。
您可以一次發(fā)出4個請求,并在它們返回時處理響應,這將使您更快地獲取頁面。
2.1 創(chuàng)建一個線程
您可以通過調用Thread.new
創(chuàng)建一個新的Ruby線程。確保傳遞帶有該線程需要運行的代碼的塊。
實例:
Thread.new { puts "hello from thread" }
# ---- 輸出結果 ----
是不是很簡單。
但你會發(fā)現(xiàn),線程沒有輸出內容,這是因為Ruby 不等待線程完成。
您需要在線程上調用join
方法來修復上面的代碼。
實例:
Thread.new { puts "hello from thread" }.join
# ---- 輸出結果 ----
hello from thread
如果要創(chuàng)建多個線程,可以將它們放入數(shù)組中,并在每個線程上調用join
。
實例:
Thread.new { puts "hello from thread1" }.join
Thread.new { puts "hello from thread2" }.join
Thread.new { puts "hello from thread3" }.join
# ---- 輸出結果 ----
hello from thread1
hello from thread2
hello from thread3
學習Ruby的線程時,我們要多參考 Ruby 線程的文檔。
2.2 線程與異常
如果線程內發(fā)生異常,它將在不停止程序或不顯示任何錯誤消息的情況下靜默死。
實例:
Thread.new { raise 'hell' }
# ---- 輸出結果 ----
為了進行調試,您可能希望程序在發(fā)生不良情況時停止運行。
為此,您可以將 Thread 上的以下標志設置為 true:
Thread.abort_on_exception = true
在創(chuàng)建線程之前,請確保設置此標志。
實例:
Thread.abort_on_exception = true
Thread.new { raise 'hell' }
sleep(1)
# ---- 輸出結果 ----
ruby.rb:2:in `block in <main>': hell (RuntimeError)
**注意事項:**這里需要增加sleep(1)
,否則不會拋出異常。
2.3 線程池
假設您要處理數(shù)百個項目,為每個項目啟動一個線程將破壞您的系統(tǒng)資源。
它看起來像這樣:
pages_to_crawl = %w( index about contact ... )
pages_to_crawl.each do |page|
Thread.new { puts page }
end
如果這樣做,您將與服務器啟動數(shù)百個連接,因此這可能不是一個好主意。
一種解決方案是使用線程池。線程池使您可以在任何給定時間控制活動線程的數(shù)量。
您可以建立自己的池,但是我不建議你這樣去做,Ruby有一個Gem可以為您完成這個操作。
實例:
require 'celluloid'
class Worker
include Celluloid
def process_page(url)
puts url
end
end
pages_to_crawl = %w( index about contact products ... )
worker_pool = Worker.pool(size: 5)
# If you need to collect the return values check out 'futures'
pages_to_crawl.each do |page|
worker_pool.process_page(page)
end
這次只有5個線程在運行,完成后他們將選擇下一個項目。
2.4 資源競爭風險
您必須知道并發(fā)代碼存在一些問題,例如,線程容易出現(xiàn)資源競爭狀況,比如同一時刻操縱了一個變量。競爭條件是當事情發(fā)生混亂并弄亂時。
另一個問題是死鎖(deadlock)這是當一個線程擁有對某個資源的獨占訪問權(使用互斥鎖(mutex)之類的鎖定系統(tǒng))而從未釋放它時,這使得所有其他線程都無法訪問它。
為避免這些問題,最好避免使用原始線程,并堅持使用一些已經為您處理好細節(jié)的Gem。
3. 小結
本章節(jié)中我們學習到了如何使用 Ruby 來創(chuàng)建一個線程。如何讓創(chuàng)建的線程拋出異常,線程池是什么,線程中存在的風險有什么。