Ruby 駭客指南/執行緒
仔細想想,我還沒有展示在實踐中使用 Ruby 執行緒的例子。現在只是個簡單的介紹
Thread.new {
while true
puts 'from thread'
end
}
while true
puts 'from main'
end
如果您執行此程式,您應該在輸出中看到“來自執行緒”和“來自主執行緒”混合在一起。
當然,除了建立多個執行緒之外,還有很多方法可以控制它們。沒有像 Java 中那樣的synchronize關鍵字,但提供了像Mutex、Queue和Monitor這樣的常用原語,下面的 API 可以用於對執行緒本身的操作。
Thread.pass - 將執行傳遞給另一個執行緒
Thread.kill(th) - 結束執行緒th
Thread.exit - 結束此執行緒
Thread.stop - 暫時暫停此執行緒
Thread#join - 等待接收執行緒結束
Thread#wakeup - 恢復先前暫停的執行緒
乍一看,執行緒似乎都是一起執行的,但實際上它們是輪流執行的,每個執行緒都執行一小段時間。嚴格來說,在多 CPU 機器上,可以同時執行多個執行緒,但即使如此,如果執行緒數超過 CPU 數量,執行緒也必須輪流執行。
Ruby 仍然有一個 GIL(全域性直譯器鎖)。由於這個鎖,ruby 直譯器嚴格來說一次只能執行一個執行緒。但是,當一個執行緒被阻塞(例如,等待網路資料到達)時,直譯器可以在阻塞執行緒等待時切換到另一個執行緒。目前,如果您想用 ruby 真正同時執行多個執行緒,您將不得不執行多個直譯器。這種技術通常被 unicorn 等 Web 伺服器使用。已經做了很多工作來減輕 GIL 的影響,將來它可能會完全消失。不過,對於大多數目的來說,目前的情況已經足夠了。
現在我們將更詳細地討論 Ruby 執行緒的特性。在討論執行緒時,可以討論它們是否是搶佔式的。
在搶佔式執行緒系統中,即使執行緒使用者沒有明確切換執行緒,執行緒也會自行切換。反過來看,執行緒切換的時間無法由使用者控制。
另一方面,在非搶佔式執行緒系統中,只要執行緒使用者沒有明確地說“你現在可以將控制權傳遞給下一個執行緒”,執行緒就不會切換。反過來看,很明顯執行緒的使用者可以控制執行緒可以切換的位置。
這種區別也適用於程序。在這種情況下,搶佔式被認為是“優越”的方法。例如,如果一個程式存在導致它陷入無限迴圈的錯誤,程序將無法切換。換句話說,一個使用者程式可能會鎖住整個系統;這不是一件好事。Windows 3.1 以 MS-DOS 為基礎,因此它的程序切換是非搶佔式的,但 Windows 95 是搶佔式的。因此,Windows 95 更加健壯,可以說 Windows 95 比 Windows 3.1 “優越”。
那麼 Ruby 執行緒是什麼樣的呢?在 Ruby 級別,執行緒是搶佔式的,而在 C 級別,執行緒是非搶佔式的。換句話說,在編寫 C 程式碼時,您可以幾乎精確地指定執行緒切換的時間。
為什麼 Ruby 執行緒是這樣的?執行緒確實很方便,但在使用它們時必須考慮一些因素。也就是說,程式碼必須適應執行緒(程式碼必須是執行緒安全的)。也就是說,如果在 C 級別搶佔執行緒切換,我們使用的所有 C 庫都必須是執行緒安全的。
然而,實際上還有很多 C 庫還沒有執行緒安全。如果我們透過將執行緒安全作為要求來減少可以使用庫的數量,那麼為使擴充套件庫易於編寫而付出的所有努力將毫無意義。因此,對於 Ruby 來說,在 Ruby 級別使執行緒非搶佔式是理性的選擇。
我們瞭解到,在 C 級別,Ruby 執行緒是非搶佔式的。也就是說,您的執行緒執行一段時間後,它會自願放棄對另一個執行緒的控制。因此,讓我們考慮一個即將停止執行的執行執行緒。它應該將控制權傳遞給哪個執行緒?不,首先我們需要知道 Ruby 執行緒在內部是如何表示的。讓我們看看管理執行緒的變數和資料結構。
▼ 執行緒管理結構
864 typedef struct thread * rb_thread_t;
865 static rb_thread_t curr_thread = 0;
866 static rb_thread_t main_thread;
7301 struct thread {
7302 struct thread *next, *prev;
(eval.c)
由於各種原因,struct thread 變得非常大,因此我們在此重點關注重要部分。僅檢視這兩個成員next和prev(它們都是rb_thread_t結構),您可能會認為rb_thread_t是一個雙向連結串列。但實際上,它不僅僅是一個雙向連結串列;它的兩端相遇。換句話說,它是一個迴圈雙向連結串列。這是一個重要的點。當您新增靜態變數main_thread和curr_thread時,整個資料結構看起來像圖 1。
圖 1:管理執行緒的資料結構
main_thread 是程式啟動時存在的執行緒。換句話說,它是“第一個”執行緒。curr_thread 當然是當前執行緒;也就是說,當前正在執行的執行緒。main_thread 的值在整個程序操作過程中不會改變,但curr_thread 的值會迅速改變。
以這種方式形成迴圈的執行緒,選擇下一個執行緒很簡單:只需沿著next連結並選擇該執行緒即可。僅憑這一點,您就可以在一定程度上均勻地執行所有執行緒。