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

Multihilo en Ruby

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

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

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

创建 Ruby 线程

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

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

Ejemplo en línea

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

Ejemplo en línea

#!/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}"
()}1Thread.new{func1()}
()}2Thread.new{func2()}
()}1.join
()}2.join
t

El resultado de la ejecución del código es:

puts "Finaliza el #{Time.now}" 14 08:21:54 -0700 2014
Inicio el mié may1 func 14 08:21:54 -0700 2014
Inicio el mié may2 func 14 08:21:54 -0700 2014
Inicio el mié may2 func 14 08:21:55 -0700 2014
Inicio el mié may1 func 14 08:21:56 -0700 2014
Inicio el mié may2 func 14 08:21:56 -0700 2014
Inicio el mié may1 func 14 08:21:58 -0700 2014
at: mié may 14 08:22Finaliza el mié may -0700 2014

:00

1Vida del hilo

2El hilo se puede crear utilizando Thread.new, y también se puede usar el mismo sintaxis con Thread.start o Thread.fork para crear hilos.

3Después de crear un hilo, no es necesario iniciarlo, ya que se ejecutará automáticamente.

4El tipo Thread define algunos métodos para controlar los hilos. El hilo ejecuta el bloque de código en Thread.new.

5El último comando en el bloque de código del hilo es el valor del hilo, que se puede llamar a través de los métodos del hilo. Si el hilo se completa, se devuelve el valor del hilo; de lo contrario, no se devuelve un valor hasta que el hilo se complete.

6El método Thread.current devuelve el objeto que representa el hilo actual. El método Thread.main devuelve el hilo principal.

estado del hilo

Se puede ejecutar el hilo a través del método Thread.Join, que bloqueará el hilo principal hasta que el hilo actual se complete.5Hay

estado del hilovalor de retorno
ejecutablecorrer
dormirDurmiente
salidaabortando
Finalización normalfalse
Finalización por excepciónnil

Hilos y excepciones

Cuando un hilo lanza una excepción y no es capturada por rescue, el hilo generalmente se termina sin advertencia. Sin embargo, si otros hilos esperan a que este hilo finalice debido a Thread#join, también se lanzará la misma excepción a los hilos esperando.

begin
  t = Thread.new do
    Thread.pass  # El hilo principal realmente espera a join
    raise "excepción no maneja"
  end
  t.join
rescue
  p $!  # => "excepción no maneja"
end

Se utiliza la siguiente3Este método permite que el intérprete se interrumpa cuando algún hilo termina debido a una excepción.

  • Al especificar al iniciar el script-dopciones, y se ejecuta en modo depuración.

  • Se establece la marca utilizando Thread.abort_on_exception.

  • Se establece una marca en el hilo especificado utilizando Thread#abort_on_exception.

Al usar las siguientes3Este método interrumpirá todo el intérprete.

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

Control de sincronización de hilos

En Ruby, se proporcionan tres formas de implementar la sincronización,分别是:

1. A través de la clase Mutex se realiza la sincronización de hilos

2. Regulación de la transferencia de datos mediante la clase Queue para la sincronización de hilos

3. Realizar el control de sincronización mediante ConditionVariable

Se realiza la sincronización de hilos mediante la clase Mutex

Se realiza el control de sincronización de hilos mediante la clase Mutex, si se necesita un programa variable en varios hilos al mismo tiempo, se puede bloquear parte de esta variable con lock. El código es el siguiente:

Ejemplo en línea

#!/usr/bin/ruby
 
require "thread"
puts "Sincronizar Hilo"
 
@num=200
@mutex=Mutex.new
 
def comprarBoleto(num)
     @mutex.lock
          if @num>=num
               @num=@num-num
               puts "has comprado con éxito #{num} boletos"
          else
               puts "lo siento, no hay suficientes boletos"
          end
     @mutex.unlock
end
 
boleto1=Thread.new 10 do
     10.times do |valor|
     boletoNum =15
     comprarBoleto(boletoNum)
     sleep 0.01
     end
end
 
boleto2=Thread.new 10 do
     10.times do |valor|
     boletoNum =20
     comprarBoleto(boletoNum)
     sleep 0.01
     end
end
 
sleep 1
boleto1.join
boleto2.join

El resultado de la ejecución del código es:

Sincronizar Hilo
has comprado con éxito 15 boletos
has comprado con éxito 20 boletos
has comprado con éxito 15 boletos
has comprado con éxito 20 boletos
has comprado con éxito 15 boletos
has comprado con éxito 20 boletos
has comprado con éxito 15 boletos
has comprado con éxito 20 boletos
has comprado con éxito 15 boletos
has comprado con éxito 20 boletos
has comprado con éxito 15 boletos
lo siento, no hay suficientes boletos
lo siento, no hay suficientes boletos
lo siento, no hay suficientes boletos
lo siento, no hay suficientes boletos
lo siento, no hay suficientes boletos
lo siento, no hay suficientes boletos
lo siento, no hay suficientes boletos
lo siento, no hay suficientes boletos
lo siento, no hay suficientes boletos

Además de usar lock para bloquear una variable, se puede usar try_lock para bloquear una variable, y también se puede usar Mutex.synchronize para sincronizar el acceso a una variable.

La clase Queue que regula la transferencia de datos realiza la sincronización de hilos

La clase Queue representa una cola que admite hilos y puede sincronizar el acceso al final de la cola. Diferentes hilos pueden usar la misma instancia de la clase, pero no tienen que preocuparse por si los datos en la cola están sincronizados, además, el uso de la clase SizedQueue permite limitar la longitud de la cola

La clase SizedQueue puede ayudarnos de manera muy conveniente a desarrollar aplicaciones de sincronización de hilos, ya que, una vez que se añade a esta cola, no hay que preocuparse por los problemas de sincronización de hilos.

Problema clásico de productor-consumidor:

Ejemplo en línea

#!/usr/bin/ruby
 
require "thread"
puts "Prueba de cola de tamaño"
 
cola = Queue.new
 
productor = Thread.new do
     10.times do |i|
          sleep rand(i) # Hacer que el hilo duerma durante un tiempo
          cola << i
          puts "#{i} producido"
     end
end
 
consumer = Thread.new do
     10.times do |i|
          value = queue.pop
          sleep rand(i/2)
          puts "consumed #{value}"
     end
end
 
consumer.join

Salida del programa:

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 del hilo

Los hilos pueden tener variables privadas, las variables privadas de los hilos se escriben en el hilo al crearlo. Pueden ser utilizadas dentro del ámbito del hilo, pero no pueden ser compartidas fuera del ámbito del hilo.

Pero, ¿qué hacer cuando se necesitan variables locales de hilos accesibles por otros hilos o la línea principal? Ruby proporciona la creación de variables de hilo permitidas a través de nombres, al igual que ver a los hilos como tablas hash. Se pueden escribir datos usando []= y leer datos usando []. Vamos a ver el siguiente código:

Ejemplo en línea

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

El resultado de la ejecución del código anterior es:

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

La línea principal espera que el hilo secundario complete su ejecución y luego salida cada valor. 。

Nivel de prioridad del hilo

El nivel de prioridad del hilo es un factor principal que afecta la programación de hilos. Otros factores incluyen la duración del tiempo de ejecución utilizado por el CPU, la programación de grupos de hilos, etc.

Se puede obtener el nivel de prioridad de un hilo utilizando el método Thread.priority y ajustar el nivel de prioridad utilizando Thread.priority=.

El nivel de prioridad de los hilos por defecto es 0. Los hilos con mayor prioridad se ejecutan más rápido.

Un hilo puede acceder a todos los datos de su ámbito, pero ¿qué hacer si se necesita acceder a los datos de otro hilo en un hilo determinado? La clase Thread proporciona métodos para que los hilos puedan acceder a datos entre sí, puedes simplemente ver a un hilo como una tabla hash, puedes escribir datos usando []= en cualquier hilo y leer datos usando [].

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(Mutal Exclusion = 互斥锁)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。

不使用Mutax的示例

Ejemplo en línea

#!/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 的示例

Ejemplo en línea

#!/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 对象时需要注意线程死锁。

Ejemplo en línea

#!/usr/bin/ruby
require 'thread'
mutex = Mutex.new
 
cv = ConditionVariable.new
a = Thread.new {
   mutex.synchronize {
      puts "A: Tengo la sección crítica, pero esperaré a cv"
      cv.wait(mutex)
      puts "A: ¡Tengo la sección crítica de nuevo! ¡Yo mando!"
   }
}
 
puts "(Más tarde, de vuelta en la granja...)"
 
b = Thread.new {
   mutex.synchronize {
      puts "B: Now I am critical, but I am done with cv"
      cv.signal
      puts "B: I am still critical, finishing up"
   }
}
a.join
b.join

The output result of the above example is as follows:

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

Thread class methods

The complete Thread (thread) class methods are as follows:

NúmeroDescripción del método
1Thread.abort_on_exception
If its value is true, once a thread terminates due to an exception, the entire interpreter will be interrupted. Its default value is false, which means that in normal circumstances, if a thread occurs an exception and the exception is not detected by Thread#join, etc., the thread will be terminated without warning.
2Thread.abort_on_exception=
If set to trueOnce a thread terminates due to an exception, the entire interpreter will be interrupted. Return the new state
3Thread.critical
Devolver un valor booleano.
4Thread.critical=
When its value is true, thread switching will not be performed. If the current thread is suspended (stop) or interrupted by a signal, its value will automatically become false.
5Thread.current
Return the currently running thread (current thread).
6Thread.exit
Terminate the execution of the current thread and return the current thread. If the current thread is the only one, it will use exit(0) to terminate its execution.
7Thread.fork { block }
Generate a thread like Thread.new.
8Thread.kill( aThread )
Terminate the execution of the thread.
9Thread.list
Return an array of active threads that are running or suspended.
10Thread.main
Return to the main thread.
11Thread.new( [arg])* ) {| args | block }
Crear un hilo y comenzar a ejecutar. Los argumentos se pasarán sin cambios al bloque. Esto permite pasar valores a las variables locales propias del hilo al iniciar el hilo.
12Thread.pass
Transfer the running right to other threads. It does not change the state of the running thread, but gives control to other runnable threads (explicit thread scheduling).
13Thread.start( [ args ])* ) {| args | block }
Crear un hilo y comenzar a ejecutar. Los argumentos se pasarán sin cambios al bloque. Esto permite pasar valores a las variables locales propias del hilo al iniciar el hilo.
14Thread.stop
Suspender el hilo actual hasta que otro hilo despierte el hilo utilizando el método run.

Métodos de ejemplo de hilo

A continuación se muestra un ejemplo que llama al método de ejemplo de hilo join:

Ejemplo en línea

#!/usr/bin/ruby
 
thr = Thread.new do   # Ejemplo
   puts "En el segundo hilo"
   raise "Lanzar excepción"
end
thr.join   # Ejemplo de llamada al método de ejemplo join

A continuación se muestra una lista de métodos de ejemplo completa:

NúmeroDescripción del método
1thr[ name ]
Obtener los datos固有del hilo correspondientes al nombre. name puede ser una cadena o un símbolo. Si no hay datos correspondientes al nombre, devuelve nil.
2thr[ name ]=
Establecer el valor de los datos固有del hilo correspondientes al nombre, name puede ser una cadena o un símbolo. Si se establece en nil, se eliminarán los datos correspondientes del hilo.
3thr.abort_on_exception
Devolver un valor booleano.
4thr.abort_on_exception=
Si su valor es true, el intérprete completo se interrumpirá una vez que algún hilo termine debido a una excepción.
5thr.alive?
Si el hilo está "vivo", devuelve true.
6thr.exit
Terminar la ejecución del hilo. Devolver self.
7thr.join
Suspender el hilo actual hasta que el hilo self termine su ejecución. Si self termina debido a una excepción, se generará la misma excepción en el hilo actual.
8thr.key?
Si los datos固有del hilo correspondientes al nombre ya han sido definidos, se devuelve true
9thr.kill
Similares a Thread.exit
10thr.priority
Devolver la prioridad del hilo. El valor predeterminado de la prioridad es 0. Cuanto mayor sea este valor, mayor será la prioridad.
11thr.priority=
Establecer la prioridad del hilo. También se puede establecer en un número negativo.
12thr.raise( anException )
Llamar forzadamente una excepción en este hilo.
13thr.run
Reiniciar el hilo suspendido (stop). A diferencia de wakeup, realiza la transición de hilo inmediatamente. Si se utiliza este método en un proceso muerto, se generará una excepción ThreadError.
14thr.safe_level
Retorna el nivel de seguridad de self. El nivel de safe_level de la línea actual es el mismo que $SAFE.
15thr.status
Usa las cadenas "run", "sleep" o "aborting" para representar el estado del hilo activo. Si un hilo termina normalmente, devuelve false. Si termina debido a una excepción, devuelve nil.
16thr.stop?
Retorna true si el hilo está en estado de finalización (dead) o suspendido (stop).
17thr.value
Esperar hasta que el hilo self termine de ejecutar (equivalente a join) y regresar el valor del bloque del hilo. Si se produce una excepción durante la ejecución del hilo, se volverá a lanzar la excepción.
18thr.wakeup
Cambiar el estado del hilo suspendido (stop) a estado ejecutable (run). Si se llama a este método en un hilo muerto, se generará una excepción ThreadError.