¿Ruby tiene verdadero multihilo?

Sé sobre el enhebrado “cooperativo” del Ruby con hilos verdes . ¿Cómo puedo crear hilos de “nivel de sistema operativo” reales en mi aplicación para hacer uso de múltiples núcleos de CPU para el procesamiento?

Actualizado con el comentario de Jörg de septiembre de 2011

Parece que aquí confundes dos cosas muy diferentes: el lenguaje de progtwigción Ruby y el modelo específico de subprocesamiento de una implementación específica del lenguaje de progtwigción Ruby. Actualmente hay alrededor de 11 implementaciones diferentes del lenguaje de progtwigción Ruby, con modelos de subprocesamiento muy diferentes y únicos.

(Desafortunadamente, solo dos de esas 11 implementaciones están realmente listas para su uso en producción, pero para fin de año ese número probablemente subirá a cuatro o cinco.) ( Actualización : ahora son 5: MRI, JRuby, YARV (el intérprete) para Ruby 1.9), Rubinius y IronRuby).

  1. La primera implementación en realidad no tiene un nombre, lo que hace que sea bastante incómodo referirse a ella y es realmente molesto y confuso. A menudo se lo conoce como “Ruby”, que es aún más molesto y confuso que no tener ningún nombre, porque lleva a una confusión interminable entre las características del lenguaje de progtwigción Ruby y una implementación particular de Ruby.

    También se llama a veces “MRI” (por “Implementación de Ruby de Matz”), CRuby o MatzRuby.

    MRI implementa Ruby Threads como Green Threads dentro de su intérprete . Desafortunadamente, no permite que esos hilos se programen en paralelo, solo pueden ejecutar un hilo a la vez.

    Sin embargo, cualquier número de subprocesos C (subprocesos POSIX, etc.) se puede ejecutar en paralelo al subproceso de Ruby, por lo que las bibliotecas C externas o las extensiones MRI C que crean subprocesos propios aún se pueden ejecutar en paralelo.

  2. La segunda implementación es YARV (abreviatura de “Yet Another Ruby VM”). YARV implementa Ruby Threads como POSIX o Windows NT Threads , sin embargo, utiliza un Global Interpreter Lock (GIL) para garantizar que solo se pueda progtwigr un Ruby Thread en un momento dado .

    Al igual que MRI, los subprocesos C pueden ejecutarse en paralelo a Ruby Threads.

    En el futuro, es posible que el GIL se descomponga en cerraduras de grano más fino, lo que permite que cada vez más código se ejecute en paralelo, pero eso está muy lejos, ni siquiera está planeado todavía.

  3. JRuby implementa Ruby Threads como Native Threads , donde “Native Threads” en el caso de la JVM obviamente significa “JVM Threads”. JRuby no les impone ningún locking adicional. Por lo tanto, si esos subprocesos realmente pueden ejecutarse en paralelo depende de la JVM: algunas JVM implementan subprocesos de JVM como subprocesos de sistema operativo y algunos como subprocesos verdes. (Las JVM principales de Sun / Oracle utilizan exclusivamente subprocesos de sistema operativo desde JDK 1.3)

  4. XRuby también implementa Ruby Threads como Hilos de JVM . Actualización : XRuby está muerto.

  5. IronRuby implementa Ruby Threads como Native Threads , donde “Native Threads” en el caso del CLR obviamente significa “CLR Threads”. IronRuby no les impone ningún locking adicional, por lo tanto, deben ejecutarse en paralelo, siempre que su CLR sea compatible.

  6. Ruby.NET también implementa Ruby Threads como CLR Threads . Actualización: Ruby.NET está muerto.

  7. Rubinius implementa Ruby Threads como Green Threads dentro de su Máquina Virtual . Más precisamente: el Rubinius VM exporta una construcción muy ligera, muy flexible de concurrencia / paralelismo / flujo de control no local, llamada ” Tarea “, y todas las otras construcciones de concurrencia (Hilos en esta discusión, pero también Continuaciones , Actores y otras cosas ) se implementan en Ruby puro, usando Tareas.

    Rubinius no puede (actualmente) progtwigr los hilos en paralelo, sin embargo, agregar eso no es un gran problema: Rubinius ya puede ejecutar varias instancias de máquinas virtuales en varios hilos POSIX en paralelo , dentro de un proceso Rubinius. Como los Threads se implementan realmente en Ruby, pueden, como cualquier otro objeto Ruby, ser serializados y enviados a una VM diferente en un hilo POSIX diferente. (Es el mismo modelo que BEAM Erlang VM usa para la concurrencia de SMP. Ya está implementado para Rubinius Actors ).

    Actualización : la información sobre Rubinius en esta respuesta es sobre la escopeta VM, que ya no existe. La “nueva” máquina virtual C ++ no usa subprocesos verdes progtwigdos en varias máquinas virtuales (es decir, estilo Erlang / BEAM), utiliza una máquina virtual individual más tradicional con múltiples modelos de subprocesos nativos del sistema operativo, como el empleado por, por ejemplo, CLR, Mono y prácticamente todas las JVM.

  8. MacRuby comenzó como un puerto de YARV en la parte superior de Objective-C Runtime y CoreFoundation y Cocoa Frameworks. Ahora ha divergido significativamente de YARV, pero AFAIK todavía comparte el mismo modelo de subprocesamiento con YARV . Actualización: MacRuby depende del recolector de basura de manzanas que se declara obsoleto y se eliminará en las versiones posteriores de MacOSX, MacRuby no está muerto.

  9. Cardinal es una implementación de Ruby para la máquina virtual Parrot . No implementa subprocesos todavía, sin embargo, cuando lo hace, probablemente los implementará como hilos de loro . Actualización : Cardinal parece muy inactivo / muerto.

  10. MagLev es una implementación de Ruby para GemStone / S Smalltalk VM . No tengo información sobre qué tipo de subprocesamiento usa GemStone / S, qué modelo de subprocesamiento utiliza MagLev o incluso si los subprocesos aún están implementados (probablemente no).

  11. HotRuby no es una implementación completa de Ruby propia. Es una implementación de una VM de bytecode YARV en JavaScript. HotRuby no admite subprocesos (¿todavía?) Y cuando lo hace, no podrán ejecutarse en paralelo, porque JavaScript no admite el paralelismo verdadero. Sin embargo, hay una versión de HotRuby de ActionScript, y ActionScript podría ser compatible con el paralelismo. Actualización : HotRuby está muerto.

Desafortunadamente, solo dos de estas 11 implementaciones de Ruby están listas para producción: MRI y JRuby.

Por lo tanto, si quieres verdaderos hilos paralelos, JRuby es actualmente tu única opción, aunque eso no es malo: JRuby es en realidad más rápido que MRI, y podría decirse que es más estable.

De lo contrario, la solución “clásica” de Ruby es usar procesos en lugar de hilos para el paralelismo. La Biblioteca Ruby Core contiene el módulo Process con el método Process.fork , lo que hace que sea más fácil deshacerse de otro proceso Ruby. Además, la Biblioteca estándar de Ruby contiene la biblioteca Distributed Ruby (dRuby / dRb) , que permite que el código Ruby se distribuya de manera trivial en múltiples procesos, no solo en la misma máquina sino también a través de la red.

Ruby 1.8 solo tiene hilos verdes, no hay forma de crear un hilo real de “nivel del sistema operativo”. Pero, ruby ​​1.9 tendrá una nueva característica llamada fibras, que le permitirá crear hilos de nivel de sistema operativo reales. Desafortunadamente, Ruby 1.9 aún está en beta, está progtwigdo para ser estable en un par de meses.

Otra alternativa es usar JRuby. JRuby implementa threads como theads de nivel de sistema operativo, no hay “hilos verdes” en él. La última versión de JRuby es 1.1.4 y es equivalente a Ruby 1.8

Depende de la implementación:

  • RMI no tiene, YARV está más cerca.
  • JRuby y MacRuby tienen.

Ruby tiene cierres como Blocks , lambdas y Procs . Para aprovechar al máximo los cierres y núcleos múltiples en JRuby, los ejecutores de Java son útiles; para MacRuby Me gustan las colas de GCD .

Tenga en cuenta que, al poder crear subprocesos reales de “nivel de sistema operativo” , no implica que pueda utilizar múltiples núcleos de CPU para el procesamiento en paralelo. Mira los ejemplos a continuación.

Este es el resultado de un progtwig simple de Ruby que usa 3 hilos usando Ruby 2.1.0:

 (jalcazar@mac ~)$ ps -M 69877 USER PID TT %CPU STAT PRI STIME UTIME COMMAND jalcazar 69877 s002 0.0 S 31T 0:00.01 0:00.04 /Users/jalcazar/.rvm/rubies/ruby-2.1.0/bin/ruby threads.rb 69877 0.0 S 31T 0:00.01 0:00.00 69877 33.4 S 31T 0:00.01 0:08.73 69877 43.1 S 31T 0:00.01 0:08.73 69877 22.8 R 31T 0:00.01 0:08.65 

Como puede ver aquí, hay cuatro subprocesos del sistema operativo, sin embargo, solo se está ejecutando el que tiene el estado R Esto se debe a una limitación en la forma en que se implementan los hilos de Ruby.


Mismo progtwig, ahora con JRuby. Puedes ver tres hilos con el estado R , lo que significa que se están ejecutando en paralelo.

 (jalcazar@mac ~)$ ps -M 72286 USER PID TT %CPU STAT PRI STIME UTIME COMMAND jalcazar 72286 s002 0.0 S 31T 0:00.01 0:00.01 /Library/Java/JavaVirtualMachines/jdk1.7.0_25.jdk/Contents/Home/bin/java -Djdk.home= -Djruby.home=/Users/jalcazar/.rvm/rubies/jruby-1.7.10 -Djruby.script=jruby -Djruby.shell=/bin/sh -Djffi.boot.library.path=/Users/jalcazar/.rvm/rubies/jruby-1.7.10/lib/jni:/Users/jalcazar/.rvm/rubies/jruby-1.7.10/lib/jni/Darwin -Xss2048k -Dsun.java.command=org.jruby.Main -cp -Xbootclasspath/a:/Users/jalcazar/.rvm/rubies/jruby-1.7.10/lib/jruby.jar -Xmx1924M -XX:PermSize=992m -Dfile.encoding=UTF-8 org/jruby/Main threads.rb 72286 0.0 S 31T 0:00.00 0:00.00 72286 0.0 S 33T 0:00.00 0:00.00 72286 0.0 S 31T 0:00.09 0:02.34 72286 7.9 S 31T 0:00.15 0:04.63 72286 0.0 S 31T 0:00.00 0:00.00 72286 0.0 S 31T 0:00.00 0:00.00 72286 0.0 S 31T 0:00.00 0:00.00 72286 0.0 S 31T 0:00.04 0:01.68 72286 0.0 S 31T 0:00.03 0:01.54 72286 0.0 S 31T 0:00.00 0:00.00 72286 0.0 S 31T 0:00.01 0:00.01 72286 0.0 S 31T 0:00.00 0:00.01 72286 0.0 S 31T 0:00.00 0:00.03 72286 74.2 R 31T 0:09.21 0:37.73 72286 72.4 R 31T 0:09.24 0:37.71 72286 74.7 R 31T 0:09.24 0:37.80 

El mismo progtwig, ahora con MacRuby. También hay tres hilos que se ejecutan en paralelo. Esto se debe a que los subprocesos de MacRuby son subprocesos POSIX ( subprocesos reales de “nivel de sistema operativo” ) y no hay GVL

 (jalcazar@mac ~)$ ps -M 38293 USER PID TT %CPU STAT PRI STIME UTIME COMMAND jalcazar 38293 s002 0.0 R 0T 0:00.02 0:00.10 /Users/jalcazar/.rvm/rubies/macruby-0.12/usr/bin/macruby threads.rb 38293 0.0 S 33T 0:00.00 0:00.00 38293 100.0 R 31T 0:00.04 0:21.92 38293 100.0 R 31T 0:00.04 0:21.95 38293 100.0 R 31T 0:00.04 0:21.99 

Una vez más, el mismo progtwig pero ahora con el viejo RMI. Debido al hecho de que esta implementación usa hilos verdes, solo aparece un hilo

 (jalcazar@mac ~)$ ps -M 70032 USER PID TT %CPU STAT PRI STIME UTIME COMMAND jalcazar 70032 s002 100.0 R 31T 0:00.08 0:26.62 /Users/jalcazar/.rvm/rubies/ruby-1.8.7-p374/bin/ruby threads.rb 

Si está interesado en Ruby multi-threading, puede encontrar interesante mi informe Depuración de progtwigs paralelos utilizando manipuladores de horquilla .
Para una descripción general de Ruby interno, Ruby Under a Microscope es una buena lectura.
Además, Ruby Threads y Global Interpreter Lock in C en Omniref explican en el código fuente por qué los hilos Ruby no se ejecutan en paralelo.

¿Qué hay de usar drb ? No es un multi-threading real sino la comunicación entre varios procesos, pero ahora puedes usarlo en 1.8 y su fricción es bastante baja.

Dejaré que el “Monitor del sistema” responda esta pregunta. Estoy ejecutando el mismo código (a continuación, que calcula números primos) con 8 subprocesos de Ruby ejecutándose en una máquina i7 (4 núcleos con hiperhilo) en ambos casos … la primera ejecución es con:

jruby 1.5.6 (ruby 1.8.7 patchlevel 249) (2014-02-03 6586) (OpenJDK 64-Bit Server VM 1.7.0_75) [amd64-java]

El segundo es con:

ruby 2.1.2p95 (2014-05-08) [x86_64-linux-gnu]

Curiosamente, la CPU es más alta para los subprocesos JRuby, pero el tiempo de finalización es un poco más corto para el interpretado Ruby. Es algo difícil de decir en el gráfico, pero la segunda ejecución (interpretada con Ruby) usa aproximadamente la mitad de las CPU (¿sin hyperthreading?)

enter image description here

 def eratosthenes(n) nums = [nil, nil, *2..n] (2..Math.sqrt(n)).each do |i| (i**2..n).step(i){|m| nums[m] = nil} if nums[i] end nums.compact end MAX_PRIME=10000000 THREADS=8 threads = [] 1.upto(THREADS) do |num| puts "Starting thread #{num}" threads[num]=Thread.new { eratosthenes MAX_PRIME } end 1.upto(THREADS) do |num| threads[num].join end 

Si está utilizando MRI, puede escribir el código en C como una extensión o usando la gem ruby-inline.

Si realmente necesita el paralelismo en Ruby para un sistema de nivel de producción (donde no puede emplear un beta), los procesos son probablemente una mejor alternativa.
Sin embargo, definitivamente vale la pena probar los hilos debajo de JRuby primero.

Además, si está interesado en el futuro de los hilos de Ruby, es posible que este artículo le resulte útil.

Aquí hay alguna información sobre Rinda, que es la implementación de Ruby de Linda (paradigma de parallel processing y computación distribuida) http://charmalloc.blogspot.com/2009/12/linda-tuples-rinda-drb-parallel.html

Como no pudo editar esa respuesta, añada una nueva respuesta aquí.

Actualización (2017-05-08)

Este artículo es muy antiguo, y la información no sigue la banda de rodadura actual (2017), a continuación se detalla un suplemento:

  1. Opal es un comstackdor fuente-fuente de Ruby a JavaScript. También tiene una implementación de Ruby corelib, es un desarrollo actual muy activo, y existe una gran cantidad de framework frontend trabajando en él. y producción lista. Como base en javascript, no admite subprocesos paralelos.

  2. truffleruby es una implementación de alto rendimiento del lenguaje de progtwigción Ruby. Construido en GraalVM por Oracle Labs, TruffleRuby es una bifurcación de JRuby, combinándolo con código del proyecto Rubinius, y también contiene código de la implementación estándar de Ruby, MRI, desarrollo aún en vivo, no listo para producción. Esta versión de ruby ​​parece haber nacido para el rendimiento, no sé si admite hilos paralelos, pero creo que debería.