2012-04-19 10 views
6

Cuando descubrí los hilos por primera vez, intenté comprobar que realmente funcionaban como se esperaba al invocar el modo reposo en varios subprocesos, en lugar de llamar a reposo normalmente. Funcionó, y yo estaba muy feliz.¿Qué uso puedo darle a los hilos Ruby, si no son realmente paralelos?

Pero luego un amigo mío me dijo que estos hilos no eran realmente paralelos, y que dormir debe estar fingiendo.

Así que ahora me escribió esta prueba para hacer algo de procesamiento real:

class Test 
    ITERATIONS = 1000 

    def run_threads 
    start = Time.now 

    t1 = Thread.new do 
     do_iterations 
    end 

    t2 = Thread.new do 
     do_iterations 
    end 

    t3 = Thread.new do 
     do_iterations 
    end 

    t4 = Thread.new do 
     do_iterations 
    end 

    t1.join 
    t2.join 
    t3.join 
    t4.join 

    puts Time.now - start 
    end 

    def run_normal 
    start = Time.now 

    do_iterations 
    do_iterations 
    do_iterations 
    do_iterations 

    puts Time.now - start 
    end 

    def do_iterations 
    1.upto ITERATIONS do |i| 
     999.downto(1).inject(:*) # 999! 
    end 
    end 
end 

y ahora estoy muy triste, porque run_threads() no sólo no obtienen mejores resultados que run_normal, fue incluso más lento!

Entonces, ¿por qué debería complicar mi aplicación con subprocesos, si no son realmente paralelos?

** ACTUALIZACIÓN **

@ fl00r dijo que podía tomar ventaja de las discusiones si he usado para tareas de IO, por lo que escribió dos más variaciones de do_iterations:

def do_iterations 
    # filesystem IO 
    1.upto ITERATIONS do |i| 
    5.times do 
     # create file 
     content = "some content #{i}" 
     file_name = "#{Rails.root}/tmp/do-iterations-#{UUIDTools::UUID.timestamp_create.hexdigest}" 
     file = ::File.new file_name, 'w' 
     file.write content 
     file.close 

     # read and delete file 
     file = ::File.new file_name, 'r' 
     content = file.read 
     file.close 
     ::File.delete file_name 
    end 
    end 
end 

def do_iterations 
    # MongoDB IO (through MongoID) 
    1.upto ITERATIONS do |i| 
    TestModel.create! :name => "some-name-#{i}" 
    end 
    TestModel.delete_all 
end 

Los los resultados de rendimiento siguen siendo los mismos: normales> hilos.

Pero ahora no estoy seguro de si mi VM puede usar todos los núcleos. Volveremos cuando lo haya probado.

+0

hilos chupa, Fibras rocas! : D – fl00r

+0

Enlazar es falso si usa ruby ​​oficial, pero para jruby y rubinius, creo que es un enhebrado real. – texasbruce

+0

El rendimiento no es la única razón para usar subprocesos. Ver mi respuesta – Phrogz

Respuesta

7

Los subprocesos podrían ser más rápidos solo si tiene un IO lento.

En Ruby tiene Global Interpreter Lock, por lo que solo puede trabajar un Thread a la vez. Por lo tanto, Ruby pasa mucho tiempo para administrar qué subproceso se debe activar en un momento (programación de subprocesos). Entonces, en su caso, cuando no hay IO, ¡será más lento!

Puede usar Rubinius o JRuby para usar hilos reales.

Ejemplo con IO:

module Test 
    extend self 

    def run_threads(method) 
    start = Time.now 

    threads = [] 
    4.times do 
     threads << Thread.new{ send(method) } 
    end 

    threads.each(&:join) 

    puts Time.now - start 
    end 

    def run_forks(method) 
    start = Time.now 

    4.times do 
     fork do 
     send(method) 
     end 
    end 
    Process.waitall 

    puts Time.now - start 
    end 

    def run_normal(method) 
    start = Time.now 

    4.times{ send(method) } 

    puts Time.now - start 
    end 

    def do_io 
    system "sleep 1" 
    end 

    def do_non_io 
    1000.times do |i| 
     999.downto(1).inject(:*) # 999! 
    end 
    end 
end 

Test.run_threads(:do_io) 
#=> ~ 1 sec 
Test.run_forks(:do_io) 
#=> ~ 1 sec 
Test.run_normal(:do_io) 
#=> ~ 4 sec 

Test.run_threads(:do_non_io) 
#=> ~ 7.6 sec 
Test.run_forks(:do_non_io) 
#=> ~ 3.5 sec 
Test.run_normal(:do_non_io) 
#=> ~ 7.2 sec 

empleos IO son 4 veces más rápido en hilos y procesos mientras que los trabajos no IO en los procesos A dos veces más rápido, a continuación Hilos y métodos de sincronización.

También en Rubí presenta Fibers "corutines" ligeros e impresionante em-synchrony gem para manejar procesos asíncronos

+0

Actualicé mi publicación con dos ejemplos más haciendo IO. – HappyDeveloper

+0

Este IO es demasiado rápido para afectar el rendimiento – fl00r

+0

Revisa mi actualización con los Procesos – fl00r

4

fl00r es correcto, el bloqueo global intérprete evita múltiples subprocesos que se ejecutan al mismo tiempo en el rubí, a excepción de IO.

La biblioteca parallel es una biblioteca muy simple que es útil para operaciones verdaderamente paralelas. Instalar con gem install parallel. Aquí está el ejemplo reescrito para utilizarlo:

require 'parallel' 
class Test 
    ITERATIONS = 1000 

    def run_parallel() 
    start = Time.now 

    results = Parallel.map([1,2,3,4]) do |val| 
     do_iterations 
    end 

    # do what you want with the results ... 
    puts Time.now - start 
    end 

    def run_normal 
    start = Time.now 

    do_iterations 
    do_iterations 
    do_iterations 
    do_iterations 

    puts Time.now - start 
    end 

    def do_iterations 
    1.upto ITERATIONS do |i| 
     999.downto(1).inject(:*) # 999! 
    end 
    end 
end 

En mi ordenador (4 CPUs), Test.new.run_normal toma 4,6 segundos, mientras que Test.new.run_parallel toma 1.65 segundos.

+0

Guau, no sabía nada de esa gema. Lo intentaré – HappyDeveloper

+3

@HappyDeveloper Solo ten cuidado, generará procesos por defecto con la tubería como mecanismo de intercambio. No es un hilo y no es liviano. Y dudo que tengas alguna ventaja si usas la opción ': in_threads' con Ruby normal. –

3

El comportamiento de los hilos se define mediante la implementación. JRuby, por ejemplo, implementa subprocesos con subprocesos JVM, que a su vez utiliza subprocesos reales.

El Global Interpreter Lock solo está disponible por razones históricas. Si Ruby 1.9 simplemente hubiera introducido hilos reales de la nada, la compatibilidad con versiones anteriores se habría roto, y habría retrasado aún más su adopción.

This answer por Jörg W Mittag ofrece una excelente comparación entre los modelos de subprocesamiento de varias implementaciones de Ruby. Elija uno que sea apropiado para sus necesidades.

Dicho esto, las discusiones se pueden utilizar para esperar a que un proceso hijo para terminar:

pid = Process.spawn 'program' 
thread = Process.detach pid 

# Later... 
status = thread.value.exitstatus 
2

Incluso si las roscas no se ejecutan en paralelo que puede ser una manera muy eficaz, sencillo de llevar a cabo algunas tareas , como trabajos de tipo cron en proceso. Por ejemplo:

Thread.new{ loop{ download_nightly_logfile_data; sleep TWENTY_FOUR_HOURS } } 
Thread.new{ loop{ send_email_from_queue; sleep ONE_MINUTE } } 
# web server app that queues mail on actions and shows current log file data 

También utilizo Temas en un servidor DRB al manejar cálculos de larga ejecución de una de mis aplicaciones web. El servidor web inicia un cálculo en un hilo y continúa respondiendo inmediatamente a las solicitudes web. Puede examinar periódicamente el estado del trabajo y ver cómo avanza. Para más detalles, lea DRb Server for Long-Running Web Processes.

1

Para una manera simple de ver la diferencia, el uso del sueño en lugar de la IO que también depende de muchas variables:

class Test 


ITERATIONS = 1000 

    def run_threads 
    start = Time.now 
    threads = [] 

    20.times do 
     threads << Thread.new do 
     do_iterations 
     end 
    end 

    threads.each {|t| t.join } # also can be written: threads.each &:join 

    puts Time.now - start 
    end 

    def run_normal 
    start = Time.now 

    20.times do 
     do_iterations 
    end 

    puts Time.now - start 
    end 

    def do_iterations 
    sleep(10) 
    end 
end 

esto tendrá una diferencia entre la solución roscado incluso en MRB, con el GIL

Cuestiones relacionadas