English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

多线程Ruby

每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。

线程是程序中的一个单一的顺序控制流程,在单个程序中同时运行多个线程完成不同的工作,称为多线程。

在 Ruby 中,我们可以通过 Thread 类来创建多线程,Ruby 的线程是一个轻量级的,可以以高效的方式实现并行代码。

创建 Ruby 线程

要启动一个新的线程,只需要调用 Thread.new 即可:

# 线程 #1 代码部分
Thread.new {
  # 线程 #2 执行代码
}
# 线程 #1 执行代码

Exemple en ligne

以下示例展示了如何在Ruby程序中使用多线程:

Exemple en ligne

#!/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 "Started At #{Time.now}"
t1Thread.new{func1()}
t2Thread.new{func2()}
t1.join
t2.join
puts "Fin à #{Time.now}"

Le résultat de l'exécution du code suivant est :

Débuté le mer mai 14 08:21:54 -0700 2014
func1 at: mer mai 14 08:21:54 -0700 2014
func2 at: mer mai 14 08:21:54 -0700 2014
func2 at: mer mai 14 08:21:55 -0700 2014
func1 at: mer mai 14 08:21:56 -0700 2014
func2 at: mer mai 14 08:21:56 -0700 2014
func1 at: mer mai 14 08:21:58 -0700 2014
Fin le mer mai 14 08:22:00 -0700 2014

Cycle de vie du thread

1La création d'un thread peut être réalisée à l'aide de Thread.new, et peut également être réalisée avec les mêmes syntaxes Thread.start ou Thread.fork pour créer un thread.

2Après la création d'un thread, il n'est pas nécessaire de le démarrer, il s'exécute automatiquement.

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

4La dernière instruction d'un bloc de code de thread est la valeur du thread, qui peut être appelée par les méthodes du thread. Si le thread s'est terminé, la valeur du thread est retournée, sinon, aucune valeur n'est retournée jusqu'à ce que le thread soit terminé.

5La méthode Thread.current retourne un objet représentant le thread courant. La méthode Thread.main retourne le thread principal.

6Exécuter un thread à l'aide de la méthode Thread.Join, cette méthode suspend le thread principal jusqu'à ce que le thread courant soit terminé.

État du thread

Un thread peut5Il y a plusieurs états :

État du threadValeur de retour
Exécutablerun
SommeilSleeping
Sortieaborting
Arrêt normalfalse
Arrêt par exceptionnil

Threads et exceptions

Lorsqu'un thread rencontre une exception qui n'est pas capturée par rescue, ce thread est généralement terminé sans avertissement. Cependant, si d'autres threads attendent ce thread en raison de Thread#join, ces threads seront également soumis à la même exception.

begin
  t = Thread.new do
    Thread.pass  # Le thread principal attend vraiment join
    raise "exception non gérée"
  end
  t.join
rescue
  p $!  # => "exception non gérée"
end

3En utilisant les instructions suivantes

  • Cette méthode permet à l'interpréteur de s'arrêter lorsque l'un des threads se termine par une exception.-dEn spécifiant cette option lors du lancement du script.

  • Définir le drapeau avec Thread.abort_on_exception.

  • Définir un drapeau pour le thread spécifié en utilisant Thread#abort_on_exception.

Lors de l'utilisation de l'une des options ci-dessus, et du mode débogage lors de l'exécution.3Après l'une de ces méthodes, l'interpréteur est interrompu en totalité.

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

Contrôle de synchronisation des threads

En Ruby, trois méthodes d'implémentation de la synchronisation sont fournies, à savoir :

1Par l'implémentation de la classe Mutex, la synchronisation des threads est réalisée.

2. La classe Queue pour superviser la transmission des données implémente la synchronisation des threads

3. Utiliser ConditionVariable pour la synchronisation

La synchronisation des threads est réalisée à l'aide de la classe Mutex

La synchronisation des threads est réalisée à l'aide de la classe Mutex, si plusieurs threads ont besoin d'un programme variable en même temps, cette variable peut être verrouillée avec lock. Code suivant :

Exemple en ligne

#!/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
 
billet1=Thread.new 10 do
     10.times do |value|
     billetNum =15
     buyTicket(billetNum)
     sleep 0.01
     end
end
 
billet2=Thread.new 10 do
     10.times do |value|
     billetNum =20
     buyTicket(billetNum)
     sleep 0.01
     end
end
 
sleep 1
billet1.join
billet2.join

Le résultat de l'exécution du code suivant est :

Synchroniser le Thread
Vous avez acheté avec succès 15 billets
Vous avez acheté avec succès 20 billets
Vous avez acheté avec succès 15 billets
Vous avez acheté avec succès 20 billets
Vous avez acheté avec succès 15 billets
Vous avez acheté avec succès 20 billets
Vous avez acheté avec succès 15 billets
Vous avez acheté avec succès 20 billets
Vous avez acheté avec succès 15 billets
Vous avez acheté avec succès 20 billets
Vous avez acheté avec succès 15 billets
Désolé, pas assez de billets
Désolé, pas assez de billets
Désolé, pas assez de billets
Désolé, pas assez de billets
Désolé, pas assez de billets
Désolé, pas assez de billets
Désolé, pas assez de billets
Désolé, pas assez de billets
Désolé, pas assez de billets

En plus de verrouiller une variable avec un lock, on peut utiliser try_lock pour verrouiller une variable, ou utiliser Mutex.synchronize pour synchroniser l'accès à une variable.

La classe Queue pour superviser la transmission des données implémente la synchronisation des threads

La classe Queue représente une file qui supporte les threads, permettant une synchronisation d'accès à la fin de la file. Différents threads peuvent utiliser la même instance de la classe, mais sans se soucier de la synchronisation des données dans cette file. De plus, l'utilisation de la classe SizedQueue permet de limiter la longueur de la file

La classe SizedQueue peut grandement faciliter le développement d'applications synchronisées entre threads, car il n'est pas nécessaire de s'inquiéter de la synchronisation des threads une fois que l'élément est ajouté à cette file.

Problème classique producteur-consommateur :

Exemple en ligne

#!/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 pendant un certain temps
          queue << i
          puts "#{i} produced"
     end
end
 
consumer = Thread.new do
     10.times do |i|
          value = queue.pop
          sleep rand(i/2)
          puts "consommé #{value}"
     end
end
 
consumer.join

Sortie du programme :

Test de SizedQuee
0 produit
1 produit
consommé 0
2 produit
consommé 1
consommé 2
3 produit
consommé 34 produit
consommé 4
5 produit
consommé 5
6 produit
consommé 6
7 produit
consommé 7
8 produit
9 produit
consommé 8
consommé 9

Variables de thread

Les threads peuvent avoir des variables privées. Les variables privées des threads sont écrites au moment de la création du thread. Elles peuvent être utilisées à l'intérieur du domaine du thread, mais ne peuvent pas être partagées à l'extérieur du thread.

Mais que faire lorsque les variables locales d'un thread doivent être accédées par d'autres threads ou le thread principal ? Ruby permet de créer des variables de thread via des noms, semblables à considérer un thread comme une table de hachage. Vous pouvez écrire des données via []= et lire des données via []. Regardons le code suivant :

Exemple en ligne

#!/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}"

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

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

Le thread principal attend que le sous-thread soit exécuté, puis affiche chaque valeur séparément. 。

Priorité des threads

Le niveau de priorité des threads est l'un des principaux facteurs influençant le planificateur de threads. D'autres facteurs incluent la durée de l'exécution utilisant le CPU, la planification de groupement des threads, etc.

Vous pouvez obtenir le niveau de priorité d'un thread en utilisant la méthode Thread.priority et ajuster le niveau de priorité en utilisant Thread.priority=.

Le niveau de priorité par défaut des threads est 0. Les threads avec une priorité plus élevée s'exécutent plus rapidement.

Un thread peut accéder à toutes les données de son propre domaine, mais que faire s'il faut accéder aux données d'un autre thread dans un même thread ? La classe Thread fournit des méthodes pour l'accès mutuel des données entre les threads. Vous pouvez simplement considérer un thread comme un hash, où vous pouvez écrire des données à l'intérieur de n'importe quel thread en utilisant []= et lire des données en utilisant [].

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"]}"}

可以看到,把线程作为一个 Hash 表,使用 [] 和 []= 方法,我们实现了线程之间的数据共享。

线程互斥

Mutex(Mutual Exclusion = 互斥锁)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。

不使用Mutex的示例

Exemple en ligne

#!/usr/bin/ruby
require 'thread'
 
count1 =2 =
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}"

以上示例运行输出结果为:

count1 :  9712487
count2 :  12501239
difference : 0

使用 mutex 的示例

Exemple en ligne

#!/usr/bin/ruby
require 'thread'
mutex = Mutex.new
 
count1 =2 =
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}"

以上示例运行输出结果为:

count1 :  1336406
count2 :  1336406
difference : 0

死锁

两个以上的运算单元,双方都在等待对方停止运行,以获取系统资源,但是没有一方提前退出时,这种状况,就称为死锁。

例如,一个进程 p1占用了显示器,同时又必须使用打印机,而打印机被进程p2占用,p2又必须使用显示器,这样就形成了死锁。

当我们在使用 Mutex 对象时需要注意线程死锁。

Exemple en ligne

#!/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 à nouveau la section critique ! Je règne !"
   }
}
 
puts "(Plus tard, de retour à la ferme...)"
 
b = Thread.new {
   mutex.synchronize {
      puts "B: Maintenant, je suis dans la section critique, mais j'ai terminé avec cv"
      cv.signal
      puts "B: Je suis toujours dans la section critique, je termine"
   }
}
a.join
b.join

Le résultat de la sortie de l'exemple ci-dessus est :

A: J'ai la section critique, mais je vais attendre cv
(Plus tard, de retour à la ferme...)
B: Maintenant, je suis dans la section critique, mais j'ai terminé avec cv
B: Je suis toujours dans la section critique, je termine
A: J'ai à nouveau la section critique ! Je règne !

Méthodes de la classe Thread

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

NuméroDescription de la méthode
1Thread.abort_on_exception
Si sa valeur est true, une fois qu'un thread s'arrête en raison d'une exception, l'interpréteur est interrompu. Sa valeur par défaut est false, ce qui signifie que dans les cas normaux, si un thread rencontre une exception et que cette exception n'est pas détectée par Thread#join, etc., le thread est terminé sans avertissement.
2Thread.abort_on_exception=
Si elle est définie comme trueUne fois qu'un thread s'arrête en raison d'une exception, l'interpréteur est interrompu. Retourne le nouveau statut
3Thread.critical
Retourner une valeur booléenne.
4Thread.critical=
Lorsque sa valeur est true, le basculement de thread ne se produit pas. Si le thread courant est en attente (stop) ou qu'il y a une intervention de signal, sa valeur devient automatiquement false.
5Thread.current
Retourne le thread en cours d'exécution (thread courant).
6Thread.exit
Termine 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 comme Thread.new.
8Thread.kill(aThread)
Termine l'exécution du thread.
9Thread.list
Retourne un tableau de threads actifs ou en attente.
10Thread.main
Retour au thread principal.
11Thread.new([arg])* ) {| args | block }
Créer un thread et commencer à l'exécuter. Les valeurs seront transmises en bloc à la méthode. Cela permet de transmettre des valeurs aux variables locales固有 du thread au moment de son lancement.
12Thread.pass
La main de l'exécution est passée à d'autres threads. Elle ne change pas l'état du thread en cours d'exécution, mais transfère le contrôle à d'autres threads exécutables (planification de thread explicite).
13Thread.start( [ args ])* ) {| args | block }
Créer un thread et commencer à l'exécuter. Les valeurs seront transmises en bloc à la méthode. Cela permet de transmettre des valeurs aux variables locales固有 du thread au moment de son lancement.
14Thread.stop
Suspendre le thread courant jusqu'à ce que d'autres threads utilisent la méthode run pour réveiller à nouveau ce thread.

Méthode d'exemple de thread

Voici un exemple qui appelle la méthode d'exemple de thread join :

Exemple en ligne

#!/usr/bin/ruby
 
thr = Thread.new do # Exemple
   puts "Dans le second thread"
   raise "Lève l'exception"
end
thr.join # Exemple de méthode d'exemple join

Voici une liste complète des méthodes d'exemple :

NuméroDescription de la méthode
1thr[ name ]
Extraire les données固有 correspondant au nom dans le thread. name peut être une chaîne ou un symbole. Retourne nil si il n'y a pas de données correspondant au nom.
2thr[ name ] =
Définir la valeur des données固有 correspondant au nom dans le thread, name peut être une chaîne ou un symbole. Si nil est défini, les données correspondantes dans le thread seront supprimées.
3thr.abort_on_exception
Retourner une valeur booléenne.
4thr.abort_on_exception=
Si cette valeur est true, l'interpréteur entier sera interrompu une fois qu'un thread se termine par une exception.
5thr.alive?
Retourne true si le thread est 'vivant'.
6thr.exit
Terminer l'exécution du thread. Retourner self.
7thr.join
Suspendre le thread courant jusqu'à ce que le thread self termine son exécution. Si self termine par une exception, l'exception sera déclenchée sur le thread courant.
8thr.key?
Retourne true si les données固有 du thread correspondant au nom ont déjà été définies.
9thr.kill
Similaire à Thread.exit
10thr.priority
Retourner la priorité du thread. La valeur par défaut de la priorité est 0. Plus cette valeur est grande, plus la priorité est élevée.
11thr.priority=
Définir la priorité du thread. Il peut également être définie sur un nombre négatif.
12thr.raise( anException )
Lève une exception forceément dans ce thread.
13thr.run
Redémarrer le thread suspendu (stop). Contrairement à wakeup, il effectue immédiatement le basculement du thread. Si cette méthode est utilisée pour un processus mort, une exception ThreadError sera déclenchée.
14thr.safe_level
返回self的安全等级。当前线程的safe_level与$SAFE相同。
15thr.status
使用字符串"run"、"sleep"或"aborting"来表示活线程的状态。如果某线程是正常终止的话,就返回false。如果因异常而终止的话,就返回nil。
16thr.stop?
如果线程处于终止状态(dead)或被挂起(stop)时,返回true。
17thr.value
等待self线程终止运行(等同于join)后,返回该线程的块的返回值。如果在线程运行过程中发生了异常,就会再次引发该异常。
18thr.wakeup
更改被挂起(stop)的线程的状态为可执行状态(run)。如果在执行该方法时对死线程进行操作,将会引发ThreadError异常。