1. ホーム
  2. スクリプト・コラム
  3. ルビートピックス

Rubyの並列処理とグローバルロック

2022-01-16 04:50:45

前置き

この記事は主にrubyの並列処理とグローバルロックに関する内容を紹介し、参考と学習のために共有しています。

並行処理と並列処理

開発現場では、「並行処理」と「並列処理」という2つの概念をよく目にしますが、並行処理と並列処理について書かれたもののほとんどに、あることが書かれています。では、この言葉をどう理解すればよいのでしょうか。

  • 同時並行です。シェフが2人の顧客から同時に2つの注文を受け、それを処理する必要があります。
  • {を使用します。 順次実行。シェフが一人しかいない場合、次から次へとメニューをこなすしかない。
  • 並列実行。料理人が2人いる場合、2人一緒に並行して調理することができます。

この例を私たちのWeb開発に拡張すると、次のように理解することができます。

  • 並行処理:サーバーは2つのクライアントから同時にリクエストを受ける。
  • {を使用します。 順次実行:サーバーにはリクエストを処理するプロセス(スレッド)が1つしかなく、2番目のリクエストが完了する前に1番目のリクエストが完了するため、2番目のリクエストは待たなければならない。
  • 並列実行。サーバーはリクエストを処理するために2つのプロセス(スレッド)を持ち、両方のリクエストは順次問題なく応答されます。

上記の例を踏まえて、このような並列動作をrubyでシミュレートするにはどうしたらよいでしょうか。次のコードを見てください。

1. 順次実行。

スレッドが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 
## user system total real
## sleep 3 seconds in f1
## sleep 2 seconds in f2
## 0.000000 0.000000 0.000000 ( 5.009620)

上のコードは、スリープを使って時間のかかる操作をシミュレートしているだけのシンプルなものです。シーケンシャル実行時に消費される時間。

2. 並列実行

マルチスレッド時の動作をシミュレート

# Pick up the above code
Benchmark.bm do |b|
 b.report do
 threads = []
 threads << Thread.new { f1 }
 threads << Thread.new { f2 }
 threads.each(&:join)
 end 
end
## user system
## 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を処理できる。 RubyのマルチスレッドはIOブロックに対応できるため、あるスレッドがIOブロックに入った場合でも、他のスレッドが実行を継続することができ、全体の処理時間を大幅に短縮することができます。

Rubyのスレッド

上記のコード例では、rubyのスレッドクラスThreadを使用していますが、RubyではThreadライクなマルチスレッドプログラムを簡単に記述することができます。 {Ruby の Thread は軽量で効率的に並列処理を実装する方法です。 /Ruby thread is a lightweight and efficient way to implement parallelism in your code.Rubyスレッドはあなたのコードに並列処理を実装するための軽量で効率的な方法です。

{次のセクションでは、並行処理のシナリオを説明します。 次に、並行処理のシナリオを説明します。

 def thread_test
 time = Time.now
 threads = 3.times.map do 
  Thread.new do
  sleep 3 
  end
 end
 puts "Don't have to wait 3 seconds to see me:#{Time.now - time}"
 threads.map(&:join)
 puts "Now I need to wait 3 seconds to see me:#{Time.now - time}"
 end
 test
 ## Don't have to wait 3 seconds to see me:8.6e-05
 ## Now you need to wait 3 seconds to see me:3.003699

Threadはノンブロッキングで生成されるため、テキストはすぐに出力されます。これは並行動作をシミュレートしています。各スレッドは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という仕組みを使っています。

複数のスレッドを使用してコードを並列化したくても、グローバルロックのため、一度に実行できるスレッドは1つだけで、どのスレッドが実行できるかは、基盤となるOSの実装に依存します。 {どのスレッドが実行できるかについては、基本的なOSの実装に依存します。

複数のCPUがあっても、各スレッドが実行できるオプションが少し増えるだけなのです。

上記のコードでは、一度に1つのスレッドだけが count += 1 を実行することができます。

Rubyのマルチスレッドは複数のCPUを再利用するわけではないので、マルチスレッドに費やす全体の時間は減りませんが、スレッド切り替えの影響により若干増えることがあります。

でも、寝ているときは明らかに並列化しているんですよ!?

これはRubyの先進的な設計で、ファイルの読み書きやネットワークリクエストなど、すべてのブロック操作が並列化できるんだ。

{{コード

ネットワークリクエスト中はプログラムがブロックされますが、このブロックはRubyの下で並列化されているので、かかる時間は大幅に短縮されます。

GILについて考える

では、このGILロックがあれば、私たちのコードはスレッドセーフということになるのでしょうか?

残念ながらそうではありません。GILはrubyの実行中のある時点で別のスレッドに切り替わるので、いくつかのクラス変数が共有されている場合は落とし穴になる可能性があります。

では、GILはどのような場合にrubyコード実行中に他のスレッドに切り替わるのでしょうか?

明確な作業ポイントがいくつかあります。

    {を使用します。 メソッド呼び出しとメソッド戻りでは、現在のスレッドのギルに対するロックがタイムアウトしたかどうか、他のスレッドにディスパッチすべきかどうかがチェックされます。 {を使用します。 すべてのio関連の操作について、他のスレッドが作業できるようにgilロックも解放される {を使用します。 c 拡張コードで gil のロックを手動で解放する。
  • もう一つわかりにくいのは、rubyのスタックもcのスタックに入るとgil検出のトリガーになることです

一例

require 'benchmark'
require 'net/http'

# Simulate network requests
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)

上記のrでは、eの順序は異なるが、@cの値は常に2、すなわち各スレッドで@cの現在値が保存されている。スレッドのスケジューリングがない。 {スレッドのスケジューリングはない。

上記のコードスレッドに追加すると、putsなどの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]]

これは、GILロック、データ例外をトリガーします。

概要

WebアプリケーションはIOを多用するため、Rubyのマルチプロセシング+マルチスレッドモデルを使用すると、システムのスループットが大幅に向上します。これは、Ruby のスレッドが IO Block になっても、他のスレッドが実行を継続できるため、IO Block の影響を全体的に軽減することができるからです。しかし、RubyのGIL(Global Interpreter Lock)のため、MRI Rubyでは複数スレッドによる並列計算をあまり活用できていません。

{PS. 追記 JRubyはGILを使わない真のマルチスレッド化により、IO Blockに対応し、マルチコアCPUを駆使して全体の計算を高速化できると言われており、今後さらに勉強する予定があります。

概要

上記はこの記事のすべての内容です、私はあなたの勉強や仕事のためのこの記事の内容は、特定の参照学習価値があることを願って、あなたが交換するメッセージを残すことができる質問がある場合は、BinaryDevelopをサポートしていただき、ありがとうございます。