Maison > Article > développement back-end > Explication détaillée de la concurrence, du parallélisme et du partage global de code de verrouillage dans Ruby
J'apprends Ruby récemment et je souhaite résumer et partager ce que j'ai appris. L'article suivant vous présente principalement les informations pertinentes sur la concurrence et les verrous globaux dans Ruby. L'article le présente en détail à travers un exemple de code. dans le besoin peuvent s'y référer, jetons un coup d'œil ci-dessous.
Avant-propos
Cet article présente principalement le contenu pertinent sur la concurrence Ruby et les verrous globaux, et le partage pour votre référence et votre étude. Ci-dessous Pas grand chose à dire, jetons un œil à l'introduction détaillée.
Concurrence et parallélisme
Lors du développement, nous entrons souvent en contact avec deux concepts : la concurrence et le parallélisme, presque tous en parlent Les articles sur la concurrence et le parallélisme mentionneront une chose : La concurrence n'est pas égale au parallélisme. Alors comment comprendre cette phrase
Concurrence : Le chef a reçu les menus commandés par 2 convives en même temps ? . Doit être traité.
Exécution séquentielle : s'il n'y a qu'un seul chef, alors il ne peut compléter qu'un menu après l'autre.
Exécution parallèle : S'il y a deux chefs, alors ils peuvent cuisiner en parallèle et deux personnes cuisinent ensemble
En étendant cet exemple à notre développement web, cela peut se comprendre ainsi :
1. Exécution séquentielle :
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)Le code ci-dessus est très simple. Utilisez sleep pour simuler des opérations fastidieuses.
Exécution parallèle
Simuler le fonctionnement du multi-threading
# 接上述代码 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)
Le multi-thread de Ruby peut gérer le bloc IO Lorsqu'un thread est dans l'état IO Block, d'autres threads peuvent le faire. continuer à s'exécuter, réduisant ainsi considérablement le temps de traitement global.
Threads dans Ruby
L'exemple de code ci-dessus utilise la classe Thread en Ruby, qui peut être facilement écrit en Ruby Un programme multithread de la classe Thread Les threads Ruby sont un moyen léger et efficace d'obtenir le parallélisme dans votre code.
Ce qui suit décrit un scénario de concurrence.
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.003699Alors à ce stade, avons-nous terminé la capacité parallèle ?
Malheureusement, dans ma description ci-dessus, nous Je viens de mentionner que nous pouvons simuler le parallélisme dans une situation non bloquante. Regardons d'autres exemples :
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)
En raison de l'existence du verrouillage global (GIL) ! ! !
Verrouillage global
Le rubis que nous utilisons habituellement utilise un mécanisme appelé GIL.
Même si nous voulons utiliser plusieurs threads pour réaliser le parallélisme du code, en raison de l'existence de ce verrou global, un seul thread peut exécuter le code à la fois. Quant au thread qui peut s'exécuter, cela dépend de l'implémentation du système d'exploitation sous-jacent.
Même si nous avons plusieurs processeurs, cela ne fournit que quelques options supplémentaires pour l'exécution de chaque thread.
Dans notre code ci-dessus, un seul thread peut exécuter count += 1 à la fois.
Le multithread Ruby ne peut pas réutiliser les processeurs multicœurs. Le temps global passé n'est pas raccourci après utilisation. multi-threading, au contraire, en raison de l'impact du changement de thread, le temps passé peut légèrement augmenter.
Mais quand nous dormions avant, nous avions clairement atteint le parallélisme !
Il s'agit de la conception avancée de Ruby - toutes les opérations de blocage peuvent être parallélisées, y compris la lecture et l'écriture de fichiers et les requêtes réseau
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)
Réflexions sur GIL
Donc, depuis l'existence de ce verrou GIL, cela signifie-t-il que notre code est thread-safe ?
Malheureusement, non, GIL passera à un autre thread de travail à certains travaux points lors de l'exécution de Ruby. Si certaines variables de classe sont partagées, cela peut causer des problèmes.
那么, 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 加快整体运算速度,有计划了解一些.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!