Ruby Multihilo

Cada programa que se ejecuta en el sistema es un proceso. Cada proceso contiene uno o más hilos.

Un hilo es un flujo de control secuencial único dentro de un programa. Ejecutar varios hilos simultáneamente en un solo programa para realizar diferentes tareas se denomina multihilo.

En Ruby, podemos crear múltiples hilos mediante la clase Thread. Los hilos de Ruby son ligeros y pueden implementar código paralelo de manera eficiente.


Crear hilos de Ruby

Para iniciar un nuevo hilo, solo necesita llamar a Thread.new:

#Código del hilo #1 Thread.new { #Ejecución del código del hilo #2 } #Ejecución del código del hilo #1

Ejemplo

El siguiente ejemplo muestra cómo usar múltiples hilos en un programa Ruby:

Ejemplo

#!/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}" t1=Thread.new{func1()} t2=Thread.new{func2()} t1.join t2.join puts "End at #{Time.now}"

Pruébalo »

El resultado de ejecutar el código anterior es:

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

Ciclo de vida de un hilo

1. Para crear un hilo se puede usar Thread.new, y también se pueden usar Thread.start o Thread.fork con la misma sintaxis para crear hilos.

2. Después de crear un hilo, no es necesario iniciarlo; el hilo se ejecutará automáticamente.

3. La clase Thread define algunos métodos para controlar los hilos. El hilo ejecuta el bloque de código dentro de Thread.new.

4. La última sentencia del bloque de código del hilo es el valor del hilo, que se puede obtener mediante los métodos del hilo. Si el hilo ha terminado de ejecutarse, devuelve el valor del hilo; de lo contrario, no devuelve un valor hasta que el hilo haya terminado.

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

6. Use el método Thread.join para ejecutar el hilo; este método suspende el hilo principal hasta que el hilo actual haya terminado.


Estado de los hilos

Los hilos tienen 5 estados:

Estado del hiloValor de retorno
Ejecutablerun
DormidoSleeping
Salidaaborting
Terminación normalfalse
Terminación anormalnil

Hilos y excepciones

Cuando un hilo lanza una excepción y no es capturada por rescue, el hilo normalmente se termina sin advertencia. Sin embargo, si otros hilos están esperando a ese hilo mediante Thread#join, los hilos en espera también recibirán la misma excepción.

begin t = Thread.new do Thread.pass #De hecho, el hilo principal está esperando en join raise "unhandled exception" end t.join rescue p $! # => "unhandled exception" end

Usando los siguientes 3 métodos, puede hacer que el intérprete se interrumpa cuando un hilo termine debido a una excepción.

  • Especificar al iniciar el script-dla opción, y ejecutar en modo de depuración.
  • usarThread.abort_on_exceptionEstablecer la bandera.
  • UsarThread#abort_on_exceptionEstablecer la bandera en el hilo especificado.

Después de usar uno de los 3 métodos anteriores, todo el intérprete se interrumpirá.

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, que son:

1. Sincronización de hilos mediante la clase Mutex

2. Sincronización de hilos mediante la clase Queue para la transferencia supervisada de datos

3. Control de sincronización mediante ConditionVariable

Sincronización de hilos mediante la clase Mutex

El control de sincronización de hilos se implementa mediante la clase Mutex. Si varios hilos necesitan una variable de programa al mismo tiempo, se puede bloquear esa variable con lock. El código es el siguiente:

Ejemplo

#!/usr/bin/ruby require "thread" puts "Synchronize Thread" @num=200 @mutex=Mutex.new def buyTicket(num) @mutex.lock if @num>=num @num=@num-num puts "you have successfully bought #{num} tickets" else puts "sorry,no enough tickets" 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

Pruébalo »

El resultado de ejecutar el código anterior es:

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

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

Sincronización de hilos mediante la clase Queue para la transferencia supervisada de datos

La clase Queue representa una cola compatible con hilos y puede sincronizar el acceso al final de la cola. Diferentes hilos pueden usar la misma cola sin preocuparse de si los datos en la cola se sincronizan. Además, la clase SizedQueue puede limitar la longitud de la cola.

La clase SizedQueue puede ayudarnos a desarrollar aplicaciones de sincronización de hilos de manera muy conveniente, porque mientras se agregue a esta cola, no hay que preocuparse por la sincronización de hilos.

El clásico problema del productor-consumidor:

Ejemplo

#!/usr/bin/ruby require "thread" puts "SizedQuee Test" queue = Queue.new producer = Thread.new do 10.times do |i| sleep rand(i) #Hacer que el hilo duerma un rato queue << i puts "#{i} produced" end end consumer = Thread.new do 10.times do |i| value = queue.pop sleep rand(i/2) puts "consumed #{value}" end end consumer.join

Pruébalo »

La 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 de hilo

Los hilos pueden tener sus propias variables privadas. Las variables privadas del hilo se escriben en el hilo cuando se crea. Se pueden usar dentro del ámbito del hilo, pero no se pueden compartir fuera del hilo.

Pero a veces, ¿qué hacer si las variables locales de un hilo necesitan ser accedidas por otros hilos o por el hilo principal? Ruby proporciona la posibilidad de crear variables de hilo mediante nombres, tratando el hilo como una tabla hash. Los datos se escriben mediante []= y se leen mediante []. Veamos el siguiente código:

Ejemplo

#!/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 ejecutar el código anterior es:

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

El hilo principal espera a que los hilos secundarios terminen de ejecutarse y luego genera cada valor por separado.


Prioridad de hilos

La prioridad de un hilo es el factor principal que afecta la programación de hilos. Otros factores incluyen el tiempo de ejecución de CPU ocupado, la programación por grupos de hilos, etc.

Puede usar el método Thread.priority para obtener la prioridad del hilo y el método Thread.priority= para ajustar la prioridad del hilo.

La prioridad predeterminada de un hilo es 0. Los hilos con mayor prioridad se ejecutan más rápido.

Un Thread puede acceder a todos los datos dentro de su propio ámbito, pero ¿qué hacer si necesita acceder a datos de otros hilos dentro de un hilo? La clase Thread proporciona métodos para el acceso mutuo a los datos de los hilos. Simplemente puede tratar un hilo como una tabla Hash: en cualquier hilo puede usar []= para escribir datos y [] para leerlos.

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

Se puede ver que, al tratar un hilo como una tabla Hash y usar los métodos [] y []=, logramos compartir datos entre hilos.


Exclusión mutua de hilos

Mutex (Mutual Exclusion = bloqueo mutuo) es un mecanismo utilizado en programación multihilo para evitar que dos hilos lean y escriban simultáneamente el mismo recurso compartido (por ejemplo, una variable global).

Ejemplo sin usar Mutex

Ejemplo

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

El resultado de ejecutar el ejemplo anterior es:

count1 :  9712487
count2 :  12501239
difference : 0

Ejemplo usando mutex

Ejemplo

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

El resultado de ejecutar el ejemplo anterior es:

count1 :  1336406
count2 :  1336406
difference : 0

Bloqueo mutuo

Cuando dos o más unidades de cálculo esperan mutuamente a que la otra deje de ejecutarse para obtener recursos del sistema, pero ninguna sale antes, esta situación se denomina bloqueo mutuo.

Por ejemplo, un proceso p1 ocupa el monitor y además necesita usar la impresora, pero la impresora está ocupada por el proceso p2, y p2 a su vez necesita usar el monitor, formando así un bloqueo mutuo.

Cuando usamos objetos Mutex, debemos prestar atención al bloqueo mutuo de hilos.

Ejemplo

#!/usr/bin/ruby require 'thread' mutex = Mutex.new cv = ConditionVariable.new a = Thread.new { mutex.synchronize { puts "A: I have critical section, but will wait for cv" cv.wait(mutex) puts "A: I have critical section again! I rule!" } } puts "(Later, back at the ranch...)" b = Thread.new { mutex.synchronize { puts "B: Now I am critical, but am done with cv" cv.signal puts "B: I am still critical, finishing up" } } a.join b.join

El resultado del ejemplo anterior es:

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étodos de clase de Thread

La lista completa de métodos de la clase Thread es la siguiente:

N.ºDescripción del método
1Thread.abort_on_exception
Si su valor es verdadero, en cuanto un hilo termine debido a una excepción, todo el intérprete se interrumpirá. Su valor predeterminado es falso, es decir, en circunstancias normales, si un hilo lanza una excepción y esta no es detectada por Thread#join u otros medios, el hilo se termina sin advertencia.
2Thread.abort_on_exception=
Si se establece entrue, en cuanto un hilo termine debido a una excepción, todo el intérprete se interrumpirá. Devuelve el nuevo estado.
3Thread.critical
Devuelve un valor booleano.
4Thread.critical=
Cuando su valor es true, no se producirá un cambio de hilo. Si el hilo actual se suspende (stop) o una señal (signal) interviene, su valor cambiará automáticamente a false.
5Thread.current
Devuelve el hilo que se está ejecutando actualmente (el hilo actual).
6Thread.exit
Termina la ejecución del hilo actual. Devuelve el hilo actual. Si el hilo actual es el único hilo, se usará exit(0) para terminar su ejecución.
7Thread.fork { block }
Genera un hilo igual que Thread.new.
8Thread.kill( aThread )
Termina la ejecución del hilo.
9Thread.list
Devuelve un array de hilos vivos que están en estado de ejecución o suspendidos.
10Thread.main
Devuelve el hilo principal.
11Thread.new( [ arg ]* ) {| args | block }
Genera un hilo y comienza su ejecución. Los argumentos se pasan al bloque sin modificaciones. Esto permite pasar valores a las variables locales inherentes al hilo al mismo tiempo que se inicia el hilo.
12Thread.pass
Cede el derecho de ejecución a otros hilos. No cambia el estado del hilo en ejecución, sino que entrega el control a otros hilos ejecutables (programación explícita de hilos).
13Thread.start( [ args ]* ) {| args | block }
Genera un hilo y comienza su ejecución. Los argumentos se pasan al bloque sin modificaciones. Esto permite pasar valores a las variables locales inherentes al hilo al mismo tiempo que se inicia el hilo.
14Thread.stop
Suspende el hilo actual hasta que otro hilo lo despierte nuevamente usando el método run.

Métodos de instancia de hilo

El siguiente ejemplo llama al método de instanciación de hilos join:

Ejemplo

#!/usr/bin/ruby thr = Thread.new do #Instanciación puts "In second thread" raise "Raise exception" end thr.join #Llamar al método de instanciación join

La siguiente es la lista completa de métodos de instanciación:

N.ºDescripción del método
1thr[ name ]
Recupera los datos inherentes correspondientes a name dentro del hilo. name puede ser una cadena o un símbolo. Si no hay datos correspondientes a name, devuelve nil.
2thr[ name ] =
Establece el valor de los datos inherentes correspondientes a name dentro del hilo. name puede ser una cadena o un símbolo. Si se establece a nil, se eliminarán los datos correspondientes dentro del hilo.
3thr.abort_on_exception
Devuelve un valor booleano.
4thr.abort_on_exception=
Si su valor es true, una vez que un hilo termina debido a una excepción, todo el intérprete será interrumpido.
5thr.alive?
Devuelve true si el hilo está "vivo".
6thr.exit
Termina la ejecución del hilo. Devuelve self.
7thr.join
Suspende el hilo actual hasta que el hilo self termine su ejecución. Si self termina debido a una excepción, se lanzará la misma excepción en el hilo actual.
8thr.key?
Devuelve true si los datos inherentes del hilo correspondientes a name ya han sido definidos.
9thr.kill
Similar aThread.exit 。
10thr.priority
Devuelve la prioridad del hilo. El valor predeterminado de la prioridad es 0. Cuanto mayor sea el valor, mayor será la prioridad.
11thr.priority=
Establece la prioridad del hilo. También se puede establecer como un número negativo.
12thr.raise( anException )
Fuerza el lanzamiento de una excepción dentro del hilo.
13thr.run
Reinicia un hilo que ha sido suspendido (stop). A diferencia de wakeup, realizará inmediatamente un cambio de hilo. Si se usa este método en un proceso muerto, se lanzará una excepción ThreadError.
14thr.safe_level
Devuelve el nivel de seguridad de self. El safe_level del hilo actual es el mismo que $SAFE.
15thr.status
Usa las cadenas "run", "sleep" o "aborting" para representar el estado de un hilo vivo. Si un hilo termina normalmente, devuelve false. Si termina debido a una excepción, devuelve nil.
16thr.stop?
Devuelve true si el hilo está en estado de terminación (dead) o suspendido (stop).
17thr.value
Espera hasta que el hilo self termine su ejecución (equivalente a join) y luego devuelve el valor de retorno del bloque del hilo. Si ocurre una excepción durante la ejecución del hilo, se volverá a lanzar dicha excepción.
18thr.wakeup
Cambia el estado de un hilo suspendido (stop) a estado ejecutable (run). Si se ejecuta este método en un hilo muerto, se lanzará una excepción ThreadError.
Otras extensiones