首頁  >  文章  >  後端開發  >  詳解ruby中並行與全域鎖定程式碼分享

詳解ruby中並行與全域鎖定程式碼分享

巴扎黑
巴扎黑原創
2017-09-13 09:44:481617瀏覽

最近在學習ruby,想著將自己學習的內容總結一下分享出來,下面這篇文章主要給大家介紹了關於ruby中並發並行與全局鎖的相關資料,文中透過範例程式碼介紹的非常詳細,需要的朋友可以參考借鑒,下面來一起看看吧。

前言

本文主要介紹了關於ruby並發並行和全域鎖定的相關內容,分享出來供大家參考學習,下面話不多說了,來一起看看詳細的介紹吧。

並發和並行

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

  • 並發: 廚師同時接收到了2個客人點了的菜單需要處理.

  • 順序執行: 如果只有一個廚師,那麼他只能一個選單接著一個選單的去完成.

  • 並行執行: 如果有兩個廚師,那麼就可以並行,兩個人一起做菜.

將這個例子擴展到我們的web開發中, 就可以這樣理解:

  • 並發:伺服器同時收到了兩個客戶端發起的請求.

  • #順序執行:伺服器只有一個進程(執行緒)處理請求,完成了第一個請求才能完成第二個請求,所以第二個請求就需要等待.

  • 並行執行:伺服器有兩個進程(線程)處理請求,兩個請求都能得到回應,而不存在先後的問題.

根據上述所描述的例子,我們在ruby 中怎麼去模擬出這樣的一個並發行為呢? 看下面這一段程式碼:

1、順序執行:

模擬只有一個執行緒時的操作.


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

2、並行執行

模擬多執行緒時的操作


# 接上述代码
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 &#39;benchmark&#39;
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 &#39;benchmark&#39;
require &#39;net/http&#39;

# 模拟网络请求
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 執行中會某一些工作點時切換到另一個工作線程去,如果共享了一些類變量時就有可能踩坑.

那么, GIL 在 ruby代码的执行中什么时候会切换到另外一个线程去工作呢?

有几个明确的工作点:

  • 方法的调用和方法的返回, 在这两个地方都会检查一下当前线程的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 加快整体运算速度,有计划了解一些.

以上是詳解ruby中並行與全域鎖定程式碼分享的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn