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