分身之術(?) Thread
Thread 幼幼班
前言
這篇文章主要是介紹 thread 的基本概念,希望能夠讓非科系或是和我一樣半路才出家寫程式的人有幫助。
雖然寫網頁用到的機率不算高,但如果把概念套到我們日常生活上的行為,其實是蠻有趣的概念。
以前一直認為程式如果有多個 thread 的話,我的執行速度應該也可以變成 n 倍快。一直到今年我才發現不是這樣,至少在現在的 ruby 大部分情況下還是如此。
Parallel
台灣翻譯成「平行」,顧名思義就是所有 thread 會一起執行。這也是當初我對多執行緒程式的想像,因為會一起跑,所以執行速度應該也會是 n 倍快,不然我用多個 thread 有什麼意義呢?
Concurrent
台灣翻譯成「並行」。因為和中國的翻譯正好相反,想當初我個人自己被搞混了很久,這個其實是 ruby 現行版本中多執行緒的執行方式。
如果用現實的行為來呈現的話,一個廚師在做菜的行為就屬於 concurrent。比方說要完成一道義大利麵的話可以把步驟分成下面幾個:
- 洗菜
- 切菜
- 材料丟平底鍋
- 燒水煮義大利麵
- 煮好的義大利麵丟平底鍋和料一起拌炒一下
- 裝盤
但一個熟練的廚師絕對不會做完一個步驟之後才繼續下一個步驟,通常會在切菜之前就開始煮水,然後在炒其他材料的時候也順便算時間把義大麵丟下去。這種並非完全照順序來執行,但也不是真的同時做兩件事情,而是不停的切換現在正在執行的動作的行為就算是 concurrent。
而因為電腦的執行速度夠快,所以會讓你有他同時做了好多事情的錯覺。
ruby 對多執行緒的實際狀況
如同上面講的,ruby 現在面對多執行緒是 concurrent 而不是 parallel。這讓我對我程式裡面的 thread 產生了疑問,如果他不是同時在執行的話,是不是對速度上根本沒有任何影響?
於是這邊我們用計算質數的程式來比一下速度:
require 'benchmark'
require 'prime'
primes = 50_000_000
Prime.each(primes).to_a
Benchmark.bm(15) do |x|
x.report('single-threaded') do
8.times do
Prime.each(primes).to_a
end
end
x.report('multi-threaded') do
2.times.map do
Thread.new do
4.times do
Prime.each(primes).to_a
end
end
end.each(&:join)
end
end
單執行緒會去計算 5 千萬中的質數 8 次。
多執行緒則是會去分成 2 個執行緒,然後各做 4 次。
下面是執行結果:
單執行緒
6.558085秒
6.622057秒
6.708759秒
多執行緒
6.375448秒
6.461049秒
8.008214秒
這樣子果然多執行緒是寫心酸的嗎?!
GIL
Global Interpreter Lock
- 有他在一次就只會跑一個 thread,所以你只能 concurrent 不能 parallel,於是有了上面的結果。
- 簡略來說這樣做是為了確保 ruby 內部是 thread safe 的,不過即使這樣也還是有網開一面的時候。
Blocking IO
GIL 會防止 ruby code 平行執行,但是 blocking IO 並不是 ruby code 的一部份,這種時候 GIL 是會放行的,比方用下面這個簡單的程式來說明:
require 'open-uri'
2.times.map do
Thread.new do
open('http://google.com')
end
end.each(&:join)
上面那段程式會送 request 給 google,執行速度會取決於 google 的狀態 + 你的網路速度。慢的話可以很慢,還好 GIL 在這種時候不會被單一 thread 卡著,實際上在跑的時候會像是下面這樣:
- ThreadA: ya~~ 我要來連去 google 啦,在等回應的時候我先把 GIL 釋放吧
- ThreadB: GIL 現在是我的啦,我也去連 google,然後等 response…..
- 於是就有可能兩個 thread 現在都在等 response,而且都沒有佔用 GIL 了
- 於是在得到回應前,可能有個時間點同時有兩個 thread 在執行
上面那一段程式如果也去比較單和多執行緒的執行時間的話,會得到下面這樣的結果:
單執行緒
14.179605秒
11.918178秒
13.309941秒
多執行緒
5.949117秒
6.091986秒
8.468648秒
可以明顯發現這一次多執行快了很多。
ruby3 - GUILD
為了讓 ruby 可以真正 的 parallel,ruby commiter 的笹田耕一先生發表了 Guild Prototype,大略上的樣子如下:
- GUILD 裡面會至少有一個 thread
- 一個 thread 裡面會至少有一個 fiber
- 基於 GIL 所以同個 guild 裡面的多個 thread 還是不會同時跑
- 但可以同時跑多個 guild 來達成平行的效果
在今年的 Ruby Kaigi / RubyElixirConfTaiwan 2018 也都發表了演講,有興趣的話可以點進去觀看他的詳細介紹。