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

Explication détaillée de la concurrence, du parallélisme et du partage global de code de verrouillage dans Ruby

巴扎黑
巴扎黑original
2017-09-13 09:44:481634parcourir

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 :

  • Concurrency : Le serveur a reçu des requêtes initiées par deux clients en même temps

  • Exécution séquentielle : Le serveur n'a qu'un seul processus (thread) à gérer. traiter la demande et terminer la première. Une demande peut terminer la deuxième demande, la deuxième demande doit donc attendre.

  • Exécution parallèle : le serveur dispose de deux processus (threads) pour traiter la demande , et les deux requêtes peuvent obtenir une réponse sans aucun problème de séquence.

D'après l'exemple décrit ci-dessus, comment pouvons-nous simuler un tel comportement concurrent dans Ruby ? Regardez l'extrait suivant ? code :

1. Exécution séquentielle :

Simuler l'opération lorsqu'il n'y a qu'un seul thread.



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



Nous avons constaté que le temps sous multi-threading est similaire à celui de f1, comme nous l'espérions, en utilisant le multi-threading, les threads peuvent être parallélisés
# 接上述代码
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.


La création de Thread est non bloquante, le texte peut donc être affiché immédiatement. Cela simule un comportement concurrent pendant 3 secondes, dans le cas. du blocage, le multithreading peut réaliser le parallélisme.
 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

Alors à 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 :


Comme nous pouvons le voir d'ici, même si nous le faisons. la même tâche est divisée en 4 threads en parallèle, mais le temps n'est pas réduit. Pourquoi ?
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)


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


Le. Le programme est bloqué lors des requêtes réseau, et ces blocages peuvent être parallélisés lors de son exécution dans Ruby, de sorte que la consommation de temps est considérablement réduite.
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)

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!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn