私は最近 Ruby を学習しており、学んだことをまとめて共有したいと思います。次の記事では主に Ruby の同時実行性とグローバル ロックに関する関連情報をサンプル コードを通じて詳しく紹介します。参照できますので、以下を見てみましょう。
はじめに
この記事は主に Ruby の同時実行性とグローバル ロックに関する関連コンテンツを紹介し、参考と学習のために共有します。以下では多くを述べません。詳しい紹介。
並行性と並列性
開発中、私たちは並行性と並列性という 2 つの概念によく遭遇します。並行性と並列性について説明するほとんどすべての記事で、並行性は並列性と同じではないということが述べられています。この文をどう理解すればよいでしょうか?
同時実行: シェフは 2 人のゲストが注文したメニューを同時に受け取り、それらを処理する必要があります。
逐次実行: シェフが 1 人しかいない場合、彼はのみ実行できます。メニューが次々と完成しました。
並列実行: シェフが 2 人いる場合、彼らは並行して調理でき、2 人で一緒に調理できます。
この例を Web 開発に拡張すると、次のように理解できます。
同時実行性: サーバーは 2 つのクライアントによって開始されたリクエストを同時に受信しました。
逐次実行: サーバーにはリクエストを処理するプロセス (スレッド) が 1 つだけあり、最初のリクエストが完了するまで 2 番目のリクエストは完了できません。リクエストが完了したため、2 番目のリクエストを待つ必要があります。
並列実行: サーバーにはリクエストを処理するための 2 つのプロセス (スレッド) があり、順序の問題なく両方のリクエストに応答できます。
によると上記の例では、このような同時動作をシミュレートするには次のコードを見てください:
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 total real ## sleep 3 seconds in f1 ## sleep 2 seconds in f2 ## 0.000000 0.000000 0.000000 ( 5.009620)上記のコードは非常にシンプルで、時間のかかる操作をシミュレートする
マルチスレッド中の操作をシミュレートします。マルチスレッドでの消費時間と f1 時間の消費量は似ていることが分かりました。これは予想どおり、マルチスレッドを使用すると、スレッドが IO ブロック内にある場合に並列処理を実現できます。 IO ブロック状態では、他のスレッドが実行し続けることができるため、全体の処理時間が大幅に短縮されます。 Thread クラスのマルチスレッド プログラムを作成します。Ruby スレッドは、コード内で並列処理を実現するための軽量で効果的な方法です
# 接上述代码 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)
申し訳ありませんが、私の上記の説明では、非ブロッキング状況で並列処理をシミュレートできることのみを述べました。他の例を見てみましょう:
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
ここからわかるように、同じタスクを次のように分割した場合でも。 4つのスレッドが並行して実行されているのに時間が減らないのはなぜですか?
グローバルロック(GIL)の存在のせいで! ! !
グローバルロック
私たちが普段使っているRubyはGILという仕組みを使っています。
コードの並列性を実現するためにマルチスレッドを使いたくても、このグローバルロックの存在により、 time コードを実行できるのは 1 つのスレッドだけであり、どのスレッドが実行できるかは、基礎となるオペレーティング システムの実装によって異なります。
複数の CPU があるとしても、各スレッドの実行にさらにいくつかのオプションが提供されるだけです。
上記のコードでは、一度に count += 1 を実行できるスレッドは 1 つだけです。 Ruby のマルチスレッドはマルチコア CPU を再利用できません。逆に、全体の時間は短縮されません。スレッド切り替えの影響により、所要時間も若干増加する可能性があります。
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)
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 中国語 Web サイトの他の関連記事を参照してください。