web-dev-qa-db-fra.com

Sécurité des threads: Variables de classe dans Ruby

Effectuer des écritures/lectures sur des variables de classe dans Ruby n'est pas sûr pour les threads. Effectuer des écritures/lectures sur des variables d'instance semble être sûr pour les threads. Cela dit, est-il sûr pour les threads d'effectuer des écritures/lectures sur l'instance variables d'un objet classe ou métaclasse?

Quelles sont les différences entre ces trois exemples (artificiels) en termes de sécurité des threads?

EXEMPLE 1: EXCLUSION MUTUELLE

class BestUser # (singleton class)
  @@instance_lock = Mutex.new

  # Memoize instance
  def self.instance
    @@instance_lock.synchronize do
      @@instance ||= best
    end
  end
end

EXEMPLE 2: STOCKAGE VARIABLE D'INSTANCE

class BestUser # (singleton class)
  # Memoize instance
  def self.instance
    @instance ||= best
  end
end

EXEMPLE 3: STOCKAGE VARIABLE D'INSTANCE SUR METACLASS

class BestUser # (singleton class)
  # Memoize instance
  class << self
    def instance
      @instance ||= best
    end
  end
end
41

les variables d'instance ne sont pas thread-safe (et les variables de classe sont encore moins thread-safe)

Les exemples 2 et 3, tous deux avec des variables d'instance, sont équivalents et ils sont [~ # ~] pas [~ # ~] thread-safe, comme @ A déclaré VincentXie. Cependant, voici un meilleur exemple pour démontrer pourquoi ils ne le sont pas:

class Foo
  def self.bar(message)
    @bar ||= message
  end
end

t1 = Thread.new do
    puts "bar is #{Foo.bar('thread1')}"
end

t2 = Thread.new do
    puts "bar is #{Foo.bar('thread2')}"
end

sleep 2

t1.join
t2.join

=> bar is thread1
=> bar is thread1

Parce que la variable d'instance est partagée entre tous les threads, comme @VincentXie l'a déclaré dans son commentaire.

PS: les variables d'instance sont parfois appelées "variables d'instance de classe", selon le contexte dans lequel elles sont utilisées:

Lorsque self est une classe, ce sont des variables d'instance de classes (variables d'instance de classe). Lorsque self est un objet, ce sont des variables d'instance d'objets (variables d'instance). - réponse de WindorC à une question à ce sujet

12
Magne

Les exemples 2 et 3 sont exactement les mêmes. Les modules et les classes sont également des objets, et la définition d'une méthode singleton sur un objet la définit en fait sur sa classe singleton.

Cela dit, et puisque vous avez déjà établi que l'accès aux variables d'instance est thread-safe, les exemples 2 et 3 sont thread-safe. L'exemple 1 devrait également être thread-safe, mais il est inférieur aux deux autres car il nécessite une synchronisation manuelle des variables.

Cependant, si vous devez tirer parti du fait que les variables de classe sont partagées dans l'arbre d'héritage, vous devrez peut-être utiliser la première approche.


La sécurité inhérente des threads du langage Ruby dépend de l'implémentation.

L'IRM, avant 1.9, implémentait les threads au niveau VM level . Cela signifie que même si Ruby est capable de planifier l'exécution de code, rien ne fonctionne vraiment en parallèle dans un seul processus Ruby. Ruby 1.9 utilise des threads natifs synchronisés avec un verrou interprète global . Seul le contexte qui contient le verrou peut exécuter du code.

n, x = 10, 0

n.times do
  Thread.new do
    n.times do
      x += 1
    end
  end
end

sleep 1
puts x
# 100

La valeur de x est toujours cohérente en IRM. Sur JRuby, cependant, l'image change. Plusieurs exécutions du même algorithme ont donné les valeurs 76, 87, 98, 88, 94. Le résultat pourrait être n'importe quoi car JRuby utilise Java threads, qui sont de vrais threads et s'exécutent en parallèle.

Tout comme dans le langage Java Java, une synchronisation manuelle est requise pour utiliser en toute sécurité les threads dans JRuby. Le code suivant donne toujours des valeurs cohérentes pour x:

require 'thread'
n, x, mutex = 10, 0, Mutex.new

n.times do
  Thread.new do
    n.times do
      mutex.synchronize do
        x += 1
      end
    end
  end
end

sleep 1
puts x
# 100
20
Matheus Moreira

Les exemples 2 et 3 sont exactement les mêmes. Ce ne sont pas du tout des filetages.

Veuillez voir l'exemple ci-dessous.

class Foo
  def self.bar
    @bar ||= create_no
  end

  def self.create_no
    no = Rand(10000)
    sleep 1
    no
  end
end

10.times.map do
  Thread.new do
    puts "bar is #{Foo.bar}"
  end
end.each(&:join)

Son résultat n'est pas le même. Le résultat est le même lorsque vous utilisez mutex comme ci-dessous.

class Foo
  @mutex = Mutex.new

  def self.bar
    @mutex.synchronize {
      @bar ||= create_no
    }
  end

  def self.create_no
    no = Rand(10000)
    sleep 1
    no
  end
end

10.times.map do
  Thread.new do
    puts "bar is #{Foo.bar}"
  end
end.each(&:join)

Il est exécuté sur CRuby 2.3.0.

7
Vincent Xie