Multithreading en Ruby

Chaque programme exécuté sur le système est un processus. Chaque processus contient un ou plusieurs threads.

Un thread est un flux de contrôle séquentiel unique dans un programme. L'exécution simultanée de plusieurs threads dans un même programme pour accomplir différentes tâches est appelée multithreading.

En Ruby, nous pouvons créer du multithreading via la classe Thread. Les threads Ruby sont légers et peuvent implémenter du code parallèle de manière efficace.


Création de threads Ruby

Pour démarrer un nouveau thread, appelez simplement Thread.new :

#Section de code du thread n°1 Thread.new { #Thread n°2 exécute le code } #Thread n°1 exécute le code

Exemple

L'exemple suivant montre comment utiliser le multithreading dans un programme Ruby :

Exemple

#!/usr/bin/ruby def func1 i=0 while i<=2 puts "func1 at: #{Time.now}" sleep(2) i=i+1 end end def func2 j=0 while j<=2 puts "func2 at: #{Time.now}" sleep(1) j=j+1 end end puts "Démarré à #{Time.now}" t1=Thread.new{func1()} t2=Thread.new{func2()} t1.join t2.join puts "Fin à #{Time.now}"

Essayez-le »

Le résultat d'exécution du code ci-dessus est :

Started At Wed May 14 08:21:54 -0700 2014
func1 at: Wed May 14 08:21:54 -0700 2014
func2 at: Wed May 14 08:21:54 -0700 2014
func2 at: Wed May 14 08:21:55 -0700 2014
func1 at: Wed May 14 08:21:56 -0700 2014
func2 at: Wed May 14 08:21:56 -0700 2014
func1 at: Wed May 14 08:21:58 -0700 2014
End at Wed May 14 08:22:00 -0700 2014

Cycle de vie des threads

1. Thread.new peut être utilisé pour créer un thread. De même, Thread.start ou Thread.fork peuvent être utilisés avec la même syntaxe pour créer des threads.

2. Une fois qu'un thread est créé, il n'est pas nécessaire de le démarrer ; le thread s'exécutera automatiquement.

3. La classe Thread définit certaines méthodes pour contrôler les threads. Un thread exécute le bloc de code dans Thread.new.

4. La dernière instruction du bloc de code du thread est la valeur du thread, qui peut être appelée via les méthodes du thread. Si le thread a terminé son exécution, il renvoie la valeur du thread ; sinon, aucune valeur n'est renvoyée tant que le thread n'est pas terminé.

5. La méthode Thread.current renvoie l'objet représentant le thread actuel. La méthode Thread.main renvoie le thread principal.

6. Utilisez la méthode Thread.join pour exécuter des threads. Cette méthode suspend le thread principal jusqu'à ce que le thread actuel termine son exécution.


États des threads

Les threads ont 5 états :

État du threadValeur de retour
Exécutablerun
En sommeilEn sommeil
En sortieaborting
Terminaison normalefalse
Terminé en raison d'une exceptionnil

Threads et exceptions

Lorsqu'une exception se produit dans un thread et n'est pas interceptée par rescue, le thread est généralement terminé sans avertissement. Toutefois, si d'autres threads attendent ce thread en raison de Thread#join, les threads en attente verront également la même exception levée.

begin t = Thread.new do Thread.pass #Le thread principal est effectivement en attente de join raise "exception non gérée" end t.join rescue p $! # => "unhandled exception" end

En utilisant les 3 méthodes suivantes, vous pouvez faire en sorte que l'interpréteur interrompe l'exécution lorsqu'un thread se termine à cause d'une exception.

  • Spécifiez lors du démarrage du script-dl'option, et exécutez en mode débogage.
  • useThread.abort_on_exceptionDéfinissez l'indicateur.
  • UtiliserThread#abort_on_exceptionDéfinissez l'indicateur pour le thread spécifié.

Après avoir utilisé l'une des trois méthodes ci-dessus, l'interpréteur entier sera interrompu.

t = Thread.new { ... } t.abort_on_exception = true

Contrôle de synchronisation des threads

En Ruby, il existe trois façons d'atteindre la synchronisation, à savoir :

1. Implémentation de la synchronisation des threads via la classe Mutex

2. Implémentation de la synchronisation des threads via la classe Queue pour le transfert de données

3. Utilisation de ConditionVariable pour le contrôle de synchronisation

Implémentation de la synchronisation des threads avec la classe Mutex

Pour implémenter un contrôle de synchronisation des threads via la classe Mutex, si plusieurs threads ont besoin d'une variable de programme en même temps, vous pouvez verrouiller cette variable à l'aide de lock. Le code est le suivant :

Exemple

#!/usr/bin/ruby require "thread" puts "Synchroniser le thread" @num=200 @mutex=Mutex.new def buyTicket(num) @mutex.lock if @num>=num @num=@num-num puts "vous avez acheté avec succès #{num} billets" else puts "désolé, pas assez de billets" end @mutex.unlock end ticket1=Thread.new 10 do 10.times do |value| ticketNum=15 buyTicket(ticketNum) sleep 0.01 end end ticket2=Thread.new 10 do 10.times do |value| ticketNum=20 buyTicket(ticketNum) sleep 0.01 end end sleep 1 ticket1.join ticket2.join

Essayez-le »

Le résultat d'exécution du code ci-dessus est :

Synchronize Thread
you have successfully bought 15 tickets
you have successfully bought 20 tickets
you have successfully bought 15 tickets
you have successfully bought 20 tickets
you have successfully bought 15 tickets
you have successfully bought 20 tickets
you have successfully bought 15 tickets
you have successfully bought 20 tickets
you have successfully bought 15 tickets
you have successfully bought 20 tickets
you have successfully bought 15 tickets
sorry,no enough tickets
sorry,no enough tickets
sorry,no enough tickets
sorry,no enough tickets
sorry,no enough tickets
sorry,no enough tickets
sorry,no enough tickets
sorry,no enough tickets
sorry,no enough tickets

En plus d'utiliser lock pour verrouiller une variable, vous pouvez également utiliser try_lock pour verrouiller une variable, et vous pouvez utiliser Mutex.synchronize pour synchroniser l'accès à une variable.

Mise en œuvre de la synchronisation des threads avec la classe Queue pour le transfert de données

La classe Queue représente une file d'attente sûre pour les threads qui peut synchroniser l'accès à la fin de la file. Différents threads peuvent utiliser la même classe de file sans se soucier de savoir si les données de la file peuvent être synchronisées. De plus, l'utilisation de la classe SizedQueue peut limiter la longueur de la file.

La classe SizedQueue peut nous aider très facilement à développer des applications synchronisées entre threads, car tant que des éléments sont ajoutés à cette file, il n'est pas nécessaire de se soucier des problèmes de synchronisation des threads.

Le problème classique du producteur-consommateur :

Exemple

#!/usr/bin/ruby require "thread" puts "SizedQuee Test" queue = Queue.new producer = Thread.new do 10.times do |i| sleep rand(i) #Laisser le thread dormir un moment queue << i puts "#{i} produit" end end consumer = Thread.new do 10.times do |i| value = queue.pop sleep rand(i/2) puts "consommé #{value}" end end consumer.join

Essayez-le »

La sortie du programme :

SizedQuee Test
0 produced
1 produced
consumed 0
2 produced
consumed 1
consumed 2
3 produced
consumed 34 produced

consumed 4
5 produced
consumed 5
6 produced
consumed 6
7 produced
consumed 7
8 produced
9 produced
consumed 8
consumed 9

Variables de thread

Les threads peuvent avoir leurs propres variables privées. Les variables privées d'un thread sont écrites dans le thread lors de sa création. Elles peuvent être utilisées dans la portée du thread, mais ne peuvent pas être partagées en dehors du thread.

Mais parfois, que se passe-t-il si les variables locales d'un thread doivent être accessibles par d'autres threads ou le thread principal ? Ruby fournit un moyen de créer des variables de thread par nom, en traitant un thread comme une table de correspondance de style hash. Utilisez []= pour écrire des données et [] pour lire des données. Regardons le code suivant :

Exemple

#!/usr/bin/ruby count = 0 arr = [] 10.times do |i| arr[i] = Thread.new { sleep(rand(0)/10.0) Thread.current["mycount"] = count count += 1 } end arr.each {|t| t.join; print t["mycount"], ", " } puts "count = #{count}"

La sortie du code ci-dessus est :

8, 0, 3, 7, 2, 1, 6, 5, 4, 9, count = 10

Le thread principal attend que les threads enfants terminent leur exécution, puis affiche chaque valeur séparément.


Priorité des threads

La priorité d'un thread est le principal facteur influençant l'ordonnancement des threads. D'autres facteurs incluent la durée d'exécution CPU, l'ordonnancement des groupes de threads, etc.

Vous pouvez utiliser la méthode Thread.priority pour obtenir la priorité d'un thread et utiliser la méthode Thread.priority= pour ajuster la priorité du thread.

La priorité de thread par défaut est 0. Les threads de priorité plus élevée s'exécutent plus rapidement.

Un Thread peut accéder à toutes les données de sa propre portée, mais que se passe-t-il si vous devez accéder aux données d'autres threads depuis un certain thread ? La classe Thread fournit des méthodes permettant aux threads d'accéder aux données les uns des autres. Vous pouvez simplement traiter un thread comme une table de hachage, et utiliser []= dans n'importe quel thread pour écrire des données, et [] pour lire des données.

athr = Thread.new { Thread.current["name"] = "Thread A"; Thread.stop } bthr = Thread.new { Thread.current["name"] = "Thread B"; Thread.stop } cthr = Thread.new { Thread.current["name"] = "Thread C"; Thread.stop } Thread.list.each {|x| puts "#{x.inspect}: #{x["name"]}" }

On peut voir qu'en traitant un thread comme une table de hachage et en utilisant les méthodes [] et []=, nous avons réalisé le partage de données entre threads.


Exclusion mutuelle des threads

Mutex (Exclusion mutuelle = verrou mutex) est un mécanisme utilisé en programmation multithread pour empêcher deux threads de lire et écrire simultanément la même ressource partagée (comme une variable globale).

Exemple sans utiliser Mutex

Exemple

#!/usr/bin/ruby require 'thread' count1 = count2 = 0 difference = 0 counter = Thread.new do loop do count1 += 1 count2 += 1 end end spy = Thread.new do loop do difference += (count1 - count2).abs end end sleep 1 puts "count1 : #{count1}" puts "count2 : #{count2}" puts "difference : #{difference}"

La sortie de l'exemple ci-dessus est :

count1 :  9712487
count2 :  12501239
difference : 0

Exemple d'utilisation de mutex

Exemple

#!/usr/bin/ruby require 'thread' mutex = Mutex.new count1 = count2 = 0 difference = 0 counter = Thread.new do loop do mutex.synchronize do count1 += 1 count2 += 1 end end end spy = Thread.new do loop do mutex.synchronize do difference += (count1 - count2).abs end end end sleep 1 mutex.lock puts "count1 : #{count1}" puts "count2 : #{count2}" puts "difference : #{difference}"

La sortie de l'exemple ci-dessus est :

count1 :  1336406
count2 :  1336406
difference : 0

Interblocage

Lorsque deux ou plusieurs unités de calcul attendent chacune que l'autre cesse de s'exécuter afin d'obtenir des ressources système, mais qu'aucune ne se termine à l'avance, cette situation est appelée interblocage.

Par exemple, si le processus p1 occupe le moniteur et doit aussi utiliser l'imprimante, mais que l'imprimante est occupée par le processus p2, et que p2 doit utiliser le moniteur, cela crée un interblocage.

Lors de l'utilisation d'objets Mutex, nous devons être attentifs aux interblocages de threads.

Exemple

#!/usr/bin/ruby require 'thread' mutex = Mutex.new cv = ConditionVariable.new a = Thread.new { mutex.synchronize { puts "A: J'ai la section critique, mais je vais attendre cv" cv.wait(mutex) puts "A: J'ai encore la section critique ! Je commande !" } } puts "(Plus tard, de retour au ranch...)" b = Thread.new { mutex.synchronize { puts "B: Maintenant je suis en section critique, mais j'en ai fini avec cv" cv.signal puts "B: Je suis toujours en section critique, je termine" } } a.join b.join

La sortie de l'exemple ci-dessus est :

A: I have critical section, but will wait for cv
(Later, back at the ranch...)
B: Now I am critical, but am done with cv
B: I am still critical, finishing up
A: I have critical section again! I rule!

Méthodes de la classe Thread

Les méthodes complètes de la classe Thread sont les suivantes :

Numéro de sérieDescription de la méthode
1Thread.abort_on_exception
Si sa valeur est true, dès qu'un thread se termine en raison d'une exception, l'interpréteur entier sera interrompu. Sa valeur par défaut est false. Autrement dit, dans des circonstances normales, si un thread lève une exception qui n'est pas détectée par Thread#join ou similaire, le thread sera terminé sans avertissement.
2Thread.abort_on_exception=
S'il est défini surtrue, dès qu'un thread se termine en raison d'une exception, l'interpréteur entier sera interrompu. Renvoie le nouvel état.
3Thread.critical
Renvoie une valeur booléenne.
4Thread.critical=
Lorsque sa valeur est true, la commutation de threads ne se produira pas. Si le thread courant est suspendu (stop) ou est interrompu par un signal, sa valeur devient automatiquement false.
5Thread.current
Retourne le thread en cours d'exécution (thread courant).
6Thread.exit
Met fin à l'exécution du thread courant. Retourne le thread courant. Si le thread courant est le seul thread, il utilisera exit(0) pour terminer son exécution.
7Thread.fork { block }
Crée un thread exactement comme Thread.new.
8Thread.kill( aThread )
Met fin à l'exécution du thread.
9Thread.list
Retourne un tableau de threads vivants qui sont dans l'état en cours d'exécution ou suspendu.
10Thread.main
Retourne le thread principal.
11Thread.new( [ arg ]* ) {| args | block }
Crée un thread et démarre l'exécution. Les arguments sont passés au bloc tels quels. Cela permet de passer des valeurs aux variables locales du thread lors du démarrage de celui-ci.
12Thread.pass
Cède le passage à d'autres threads. Cela ne change pas l'état du thread en cours d'exécution, mais transfère le contrôle à d'autres threads exécutables (ordonnancement explicite des threads).
13Thread.start( [ args ]* ) { | args | block }
Crée un thread et démarre l'exécution. Les arguments sont passés au bloc tels quels. Cela permet de passer des valeurs aux variables locales du thread lors du démarrage de celui-ci.
14Thread
Suspend le thread courant jusqu'à ce qu'un autre thread utilise la méthode run pour le réveiller à nouveau

Méthodes d'instance de Thread

L'exemple suivant appelle la méthode d'instanciation de thread join :

Exemple

#!/usr/bin/ruby thr = Thread.new do #Instanciation puts "Dans le second thread" raise "Lève une exception" end thr.join #Appelle la méthode d'instanciation join

Voici la liste complète des méthodes d'instanciation :

Non.Description de la méthode
1thr[ name ]
Récupère les données locales au thread correspondant à name. name peut être une chaîne de caractères ou un symbole. S'il n'existe pas de données correspondant à name, nil est retourné.
2thr[ name ] =
Définit la valeur des données locales au thread correspondant à name. name peut être une chaîne de caractères ou un symbole. Si défini à nil, les données correspondantes dans le thread sont supprimées.
3thr.abort_on_exception
Renvoie une valeur booléenne.
4thr.abort_on_exception=
Si sa valeur est true, dès qu'un thread se termine en raison d'une exception, l'interpréteur entier est interrompu.
5thr.alive?
Retourne true si le thread est vivant.
6thr.exit
Termine l'exécution du thread. Retourne self.
7thr.join
Suspend le thread courant jusqu'à ce que le thread self se termine. Si self se termine à cause d'une exception, le thread courant lèvera la même exception.
8thr.key?
Renvoie true si les données locales au thread correspondant à name ont déjà été définies.
9thr.kill
Similaire àThread.exit 。
10thr.priority
Renvoie la priorité du thread. La priorité par défaut est 0. Plus la valeur est grande, plus la priorité est élevée.
11thr.priority=
Définit la priorité du thread. Elle peut également être définie sur un nombre négatif.
12thr.raise( anException )
Lève de force une exception dans le thread.
13thr.run
Redémarre un thread suspendu (stop). Contrairement à wakeup, il effectue immédiatement un changement de thread. Si cette méthode est utilisée sur un thread mort, une exception ThreadError est levée.
14thr.safe_level
Renvoie le niveau de sécurité de self. Le safe_level du thread courant est identique à $SAFE.
15thr.status
Représente l'état d'un thread vivant à l'aide des chaînes "run", "sleep" ou "aborting". Renvoie false si un thread s'est terminé normalement. Renvoie nil s'il s'est terminé en raison d'une exception.
16thr.stop?
Renvoie true si le thread est mort ou suspendu (stop).
17thr.value
Attend que le thread self se termine (équivalent à join), puis renvoie la valeur de retour du bloc du thread. Si une exception s'est produite pendant l'exécution du thread, cette exception est à nouveau levée.
18thr.wakeup
Change l'état d'un thread suspendu (stop) en exécutable (run). Si cette méthode est exécutée sur un thread mort, une exception ThreadError est levée.
Autres extensions