前言
本文主要給大家介紹了關(guān)于ruby并發(fā)并行和全局鎖的相關(guān)內(nèi)容,分享出來供大家參考學(xué)習(xí),下面話不多說了,來一起看看詳細的介紹吧。
并發(fā)和并行
在開發(fā)時,我們經(jīng)常會接觸到兩個概念: 并發(fā)和并行,幾乎所有談到并發(fā)和并行的文章都會提到一點: 并發(fā)并不等于并行.那么如何理解這句話呢?
- 并發(fā): 廚師同時接收到了2個客人點了的菜單需要處理.
- 順序執(zhí)行: 如果只有一個廚師,那么他只能一個菜單接著一個菜單的去完成.
- 并行執(zhí)行: 如果有兩個廚師,那么就可以并行,兩個人一起做菜.
將這個例子擴展到我們的web開發(fā)中, 就可以這樣理解:
- 并發(fā):服務(wù)器同時收到了兩個客戶端發(fā)起的請求.
- 順序執(zhí)行:服務(wù)器只有一個進程(線程)處理請求,完成了第一個請求才能完成第二個請求,所以第二個請求就需要等待.
- 并行執(zhí)行:服務(wù)器有兩個進程(線程)處理請求,兩個請求都能得到響應(yīng),而不存在先后的問題.
根據(jù)上述所描述的例子,我們在 ruby 中怎么去模擬出這樣的一個并發(fā)行為呢? 看下面這一段代碼:
1、順序執(zhí)行:
模擬只有一個線程時的操作.
require 'benchmark'
def f1
puts "sleep 3 seconds in f1\n"
sleep 3
end
def f2
puts "sleep 2 seconds in f2\n"
sleep 2
end
Benchmark.bm do |b|
b.report do
f1
f2
end
end
##
## user system total real
## sleep 3 seconds in f1
## sleep 2 seconds in f2
## 0.000000 0.000000 0.000000 ( 5.009620)
上述代碼很簡單,用 sleep 模擬耗時的操作.順序執(zhí)行時候的消耗時間.
2、并行執(zhí)行
模擬多線程時的操作
# 接上述代碼
Benchmark.bm do |b|
b.report do
threads = []
threads Thread.new { f1 }
threads Thread.new { f2 }
threads.each(:join)
end
end
##
## user system total real
## sleep 3 seconds in f1
## sleep 2 seconds in f2
## 0.000000 0.000000 0.000000 ( 3.005115)
我們發(fā)現(xiàn)多線程下耗時和f1的耗時相近,這與我們預(yù)期的一樣,采用多線程可以實現(xiàn)并行.
Ruby 的多線程能夠應(yīng)付 IO Block,當某個線程處于 IO Block 狀態(tài)時,其它的線程還可以繼續(xù)執(zhí)行,從而使整體處理時間大幅縮短.
Ruby 中的線程
上述的代碼示例中使用了 ruby 中 Thread 的線程類, Ruby可以很容易地寫Thread類的多線程程序.Ruby線程是一個輕量級的和有效的方式,以實現(xiàn)在你的代碼的并行.
接下來來描述一段并發(fā)時的情景
def thread_test
time = Time.now
threads = 3.times.map do
Thread.new do
sleep 3
end
end
puts "不用等3秒就可以看到我:#{Time.now - time}"
threads.map(:join)
puts "現(xiàn)在需要等3秒才可以看到我:#{Time.now - time}"
end
test
## 不用等3秒就可以看到我:8.6e-05
## 現(xiàn)在需要等3秒才可以看到我:3.003699
Thread的創(chuàng)建是非阻塞的,所以文字立即就可以輸出.這樣就模擬了一個并發(fā)的行為.每個線程sleep 3 秒,在阻塞的情況下,多線程可以實現(xiàn)并行.
那么這個時候我們是不是就完成了并行的能力呢?
很遺憾,我上述的描述中只是提到了我們在非阻塞的情況下可以模擬了并行.讓我們再看一下別的例子:
require 'benchmark'
def multiple_threads
count = 0
threads = 4.times.map do
Thread.new do
2500000.times { count += 1}
end
end
threads.map(:join)
end
def single_threads
time = Time.now
count = 0
Thread.new do
10000000.times { count += 1}
end.join
end
Benchmark.bm do |b|
b.report { multiple_threads }
b.report { single_threads }
end
## user system total real
## 0.600000 0.010000 0.610000 ( 0.607230)
## 0.610000 0.000000 0.610000 ( 0.623237)
從這里可以看出,即便我們將同一個任務(wù)分成了4個線程并行,但是時間并沒有減少,這是為什么呢?
因為有全局鎖(GIL)的存在!?。?/p>
全局鎖
我們通常使用的ruby采用了一種稱之為GIL的機制.
即便我們希望使用多線程來實現(xiàn)代碼的并行, 由于這個全局鎖的存在, 每次只有一個線程能夠執(zhí)行代碼,至于哪個線程能夠執(zhí)行, 這個取決于底層操作系統(tǒng)的實現(xiàn)。
即便我們擁有多個CPU, 也只是為每個線程的執(zhí)行多提供了幾個選擇而已。
我們上面代碼中每次只有一個線程可以執(zhí)行 count += 1 .
Ruby 多線程并不能重復(fù)利用多核 CPU,使用多線程后整體所花時間并不縮短,反而由于線程切換的影響,所花時間可能還略有增加。
但是我們之前sleep的時候, 明明實現(xiàn)了并行啊!
這個就是Ruby設(shè)計高級的地方——所有的阻塞操作是可以并行的,包括讀寫文件,網(wǎng)絡(luò)請求在內(nèi)的操作都是可以并行的.
require 'benchmark'
require 'net/http'
# 模擬網(wǎng)絡(luò)請求
def multiple_threads
uri = URI("http://www.baidu.com")
threads = 4.times.map do
Thread.new do
25.times { Net::HTTP.get(uri) }
end
end
threads.map(:join)
end
def single_threads
uri = URI("http://www.baidu.com")
Thread.new do
100.times { Net::HTTP.get(uri) }
end.join
end
Benchmark.bm do |b|
b.report { multiple_threads }
b.report { single_threads }
end
user system total real
0.240000 0.110000 0.350000 ( 3.659640)
0.270000 0.120000 0.390000 ( 14.167703)
在網(wǎng)絡(luò)請求時程序發(fā)生了阻塞,而這些阻塞在Ruby的運行下是可以并行的,所以在耗時上大大縮短了.
GIL 的思考
那么,既然有了這個GIL鎖的存在,是否意味著我們的代碼就是線程安全了呢?
很遺憾不是的,GIL 在ruby 執(zhí)行中會某一些工作點時切換到另一個工作線程去,如果共享了一些類變量時就有可能踩坑.
那么, GIL 在 ruby代碼的執(zhí)行中什么時候會切換到另外一個線程去工作呢?
有幾個明確的工作點:
- 方法的調(diào)用和方法的返回, 在這兩個地方都會檢查一下當前線程的gil的鎖是否超時,是否要調(diào)度到另外線程去工作
- 所有io相關(guān)的操作, 也會釋放gil的鎖讓其它線程來工作
- 在c擴展的代碼中手動釋放gil的鎖
- 還有一個比較難理解, 就是ruby stack 進入 c stack的時候也會觸發(fā)gil的檢測
一個例子
@a = 1
r = []
10.times do |e|
Thread.new {
@c = 1
@c += @a
r [e, @c]
}
end
r
## [[3, 2], [1, 2], [2, 2], [0, 2], [5, 2], [6, 2], [7, 2], [8, 2], [9, 2], [4, 2]]
上述中r 里 雖然e的前后順序不一樣, 但是@c的值始終保持為 2 ,即每個線程時都能保留好當前的 @c 的值.沒有線程簡的調(diào)度.
如果在上述代碼線程中加入 可能會觸發(fā)GIL的操作 例如 puts 打印到屏幕:
@a = 1
r = []
10.times do |e|
Thread.new {
@c = 1
puts @c
@c += @a
r [e, @c]
}
end
r
## [[2, 2], [0, 2], [4, 3], [5, 4], [7, 5], [9, 6], [1, 7], [3, 8], [6, 9], [8, 10]]
這個就會觸發(fā)GIL的lock, 數(shù)據(jù)異常了.
小結(jié)
Web 應(yīng)用大多是 IO 密集型的,利用 Ruby 多進程+多線程模型將能大幅提升系統(tǒng)吞吐量.其原因在于:當Ruby 某個線程處于 IO Block 狀態(tài)時,其它的線程還可以繼續(xù)執(zhí)行,從而降低 IO Block 對整體的影響.但由于存在 Ruby GIL (Global Interpreter Lock),MRI Ruby 并不能真正利用多線程進行并行計算.
PS. 據(jù)說 JRuby 去除了GIL,是真正意義的多線程,既能應(yīng)付 IO Block,也能充分利用多核 CPU 加快整體運算速度,有計劃了解一些.
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。