ruby 中的併發並行和全局鎖

併發和並行

在開發時,咱們常常會接觸到兩個概念: 併發和並行,幾乎全部談到併發和並行的文章都會提到一點: 併發並不等於並行.那麼如何理解這句話呢?web

併發: 廚師同時接收到了2個客人點了的菜單須要處理.
順序執行: 若是隻有一個廚師,那麼他只能一個菜單接着一個菜單的去完成.
並行執行: 若是有兩個廚師,那麼就能夠並行,兩我的一塊兒作菜.安全

將這個例子擴展到咱們的web開發中, 就能夠這樣理解:ruby

併發:服務器同時收到了兩個客戶端發起的請求.
順序執行:服務器只有一個進程(線程)處理請求,完成了第一個請求才能完成第二個請求,因此第二個請求就須要等待.
並行執行:服務器有兩個進程(線程)處理請求,兩個請求都能獲得響應,而不存在前後的問題.服務器

根據上述所描述的例子,咱們在 ruby 中怎麼去模擬出這樣的一個併發行爲呢? 看下面這一段代碼:網絡

  • 順序執行:
    模擬只有一個線程時的操做.多線程

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 模擬耗時的操做.順序執行時候的消耗時間.併發

  • 並行執行
    模擬多線程時的操做ui

# 接上述代碼
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)

咱們發現多線程下耗時和f1的耗時相近,這與咱們預期的同樣,採用多線程能夠實現並行.
Ruby 的多線程可以應付 IO Block,當某個線程處於 IO Block 狀態時,其它的線程還能夠繼續執行,從而使總體處理時間大幅縮短.操作系統

Ruby 中的線程

上述的代碼示例中使用了 ruby 中 Thread 的線程類, Ruby能夠很容易地寫Thread類的多線程程序.Ruby線程是一個輕量級的和有效的方式,以實如今你的代碼的並行.
接下來來描述一段併發時的情景線程

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 "如今須要等3秒才能夠看到我:#{Time.now - time}"
  end
  test
  ## 不用等3秒就能夠看到我:8.6e-05
  ## 如今須要等3秒才能夠看到我:3.003699

Thread的建立是非阻塞的,因此文字當即就能夠輸出.這樣就模擬了一個併發的行爲.每一個線程sleep 3 秒,在阻塞的狀況下,多線程能夠實現並行.

那麼這個時候咱們是否是就完成了並行的能力呢?
很遺憾,我上述的描述中只是提到了咱們在非阻塞的狀況下能夠模擬了並行.讓咱們再看一下別的例子:

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)

從這裏能夠看出,即使咱們將同一個任務分紅了4個線程並行,可是時間並無減小,這是爲何呢?
由於有全局鎖(GIL)的存在!!!

全局鎖

咱們一般使用的ruby採用了一種稱之爲GIL的機制.

即使咱們但願使用多線程來實現代碼的並行, 因爲這個全局鎖的存在, 每次只有一個線程可以執行代碼,至於哪一個線程可以執行, 這個取決於底層操做系統的實現。
即使咱們擁有多個CPU, 也只是爲每一個線程的執行多提供了幾個選擇而已。

咱們上面代碼中每次只有一個線程能夠執行 count += 1 .

Ruby 多線程並不能重複利用多核 CPU,使用多線程後總體所花時間並不縮短,反而因爲線程切換的影響,所花時間可能還略有增長。

可是咱們以前sleep的時候, 明明實現了並行啊!

這個就是Ruby設計高級的地方——全部的阻塞操做是能夠並行的,包括讀寫文件,網絡請求在內的操做都是能夠並行的.

require 'benchmark'
require 'net/http'

# 模擬網絡請求
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)

在網絡請求時程序發生了阻塞,而這些阻塞在Ruby的運行下是能夠並行的,因此在耗時上大大縮短了.

GIL 的思考

那麼,既然有了這個GIL鎖的存在,是否意味着咱們的代碼就是線程安全了呢?
很遺憾不是的,GIL 在ruby 執行中會某一些工做點時切換到另外一個工做線程去,若是共享了一些類變量時就有可能踩坑.

那麼, GILruby代碼的執行中何時會切換到另一個線程去工做呢?

有幾個明確的工做點:

  • 方法的調用和方法的返回, 在這兩個地方都會檢查一下當前線程的gil的鎖是否超時,是否要調度到另外線程去工做

  • 全部io相關的操做, 也會釋放gil的鎖讓其它線程來工做

  • 在c擴展的代碼中手動釋放gil的鎖

  • 還有一個比較難理解, 就是ruby stack 進入 c stack的時候也會觸發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 的值.沒有線程簡的調度.
若是在上述代碼線程中加入 可能會觸發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]]

這個就會觸發GIL的lock, 數據異常了.

小結

Web 應用大可能是 IO 密集型的,利用 Ruby 多進程+多線程模型將能大幅提高系統吞吐量.其緣由在於:當Ruby 某個線程處於 IO Block 狀態時,其它的線程還能夠繼續執行,從而下降 IO Block 對總體的影響.但因爲存在 Ruby GIL (Global Interpreter Lock),MRI Ruby 並不能真正利用多線程進行並行計算.

PS. 聽說 JRuby 去除了GIL,是真正意義的多線程,既能應付 IO Block,也能充分利用多核 CPU 加快總體運算速度,有計劃瞭解一些.

相關文章
相關標籤/搜索