2010-06-17 5 views
10

J'ai deux scripts qui utilisent Mechanize pour aller chercher une page d'index Google. J'ai supposé que EventMachine serait plus rapide qu'un thread Ruby, mais ce n'est pas le cas.Pourquoi le délai d'EventMachine est-il plus lent qu'un Ruby Thread?

coûts de code eventmachine: "0.24s user 0.08s system 2% cpu 12.682 total"

Ruby coûts de code du fil: "0.22s user 0.08s system 5% cpu 5.167 total "

Suis-je utiliser eventmachine dans le mauvais sens?

eventmachine:

require 'rubygems' 
require 'mechanize' 
require 'eventmachine' 

trap("INT") {EM.stop} 

EM.run do 
    num = 0 
    operation = proc { 
    agent = Mechanize.new 
    sleep 1 
    agent.get("http://google.com").body.to_s.size 
    } 
    callback = proc { |result| 
    sleep 1 
    puts result 
    num+=1 
    EM.stop if num == 9 
    } 

    10.times do 
    EventMachine.defer operation, callback 
    end 
end 

Ruby Discussion:

require 'rubygems' 
require 'mechanize' 


threads = [] 
10.times do 
    threads << Thread.new do 
    agent = Mechanize.new 
    sleep 1 
    puts agent.get("http://google.com").body.to_s.size 
    sleep 1 
    end 
end 


threads.each do |aThread| 
    aThread.join 
end 
+0

Quelle version et implémentation de ruby ​​utilisez-vous? Pour les implémentations avec un verrou GIL (Global Interpreter Lock), les threads verts peuvent ne pas s'exécuter complètement simultanément. Vous pourriez essayer d'exécuter l'exemple dans jRuby ou Rubinius pour confirmer votre comportement observé –

Répondre

9

Eh oui, vous l'utilisez mal. EventMachine fonctionne en effectuant des appels d'E/S asynchrones qui reviennent immédiatement et notifient le "réacteur" (la boucle d'événements lancée par EM.run) lorsqu'ils sont terminés. Vous avez deux appels qui bloquent le but du système, sleep et Mechanize.get. Vous devez utiliser des bibliothèques spéciales asynchrones/non bloquantes pour dériver n'importe quelle valeur de EventMachine.

+2

Vous avez raison de dire que l'exemple qu'il a posé peut être réécrit avec une bibliothèque HTTP asynchrone, mais le point de la méthode #defer est spécifiquement pour que vous puissiez apparaître un nouveau thread qui effectue une opération de blocage sans affecter la boucle d'exécution du réacteur. Donc théoriquement, son exemple ne bloque pas la boucle d'exécution. Ma conjecture avec la différence de temps est la façon dont les discussions sont planifiées. –

+0

En général, EventMachine fonctionne exactement comme vous l'avez dit, mais votre réponse ne s'applique pas à son utilisation de l'appel 'defer'. –

2

eventmachine "différez" fraye en fait des fils Ruby d'un threadpool il parvient à traiter votre demande. Oui, EventMachine est conçu pour des opérations d'E/S non bloquantes, mais la commande defer est une exception - elle est conçue pour vous permettre de faire des opérations de longue durée sans bloquer le réacteur. Donc, ça va être un peu plus lent que les threads nus, parce que ça ne fait que lancer des threads avec la surcharge du gestionnaire de threads de EventMachine.

Vous pouvez en savoir plus sur Différer ici: http://eventmachine.rubyforge.org/EventMachine.html#M000486

Cela dit, aller chercher pages est une grande utilisation de eventmachine, mais comme d'autres l'ont dit, vous avez besoin d'utiliser une bibliothèque IO non-bloquant, puis utilisez next_tick ou similaire pour démarrer vos tâches, plutôt que de reporter, ce qui casse votre tâche hors de la boucle du réacteur.

+0

Le lien FYI est rompu. –

24

Toutes les réponses à ce sujet manquent un point clé: vos rappels sont exécutés à l'intérieur du thread du réacteur et non dans un thread différé séparé. L'exécution des demandes de mécanisation dans un appel defer est la bonne façon de ne pas bloquer la boucle, mais vous devez veiller à ce que votre rappel ne bloque pas la boucle.

Lorsque vous exécutez EM.defer operation, callback, l'opération est exécutée dans un thread généré par Ruby, qui effectue le travail, puis le rappel est émis dans la boucle principale. Par conséquent, le sleep 1 dans operation s'exécute en parallèle, mais le rappel s'exécute en série. Ceci explique la différence de temps d'exécution de près de 9 secondes.

Voici une version simplifiée du code que vous utilisez.

EM.run { 
    times = 0 

    work = proc { sleep 1 } 

    callback = proc { 
    sleep 1 
    EM.stop if (times += 1) >= 10 
    } 

    10.times { EM.defer work, callback } 
} 

Cela prend environ 12 secondes, ce qui est égal à 1 seconde pour les couchages parallèles, 10 secondes pour la série dort, et 1 seconde pour les frais généraux.

Pour exécuter le code de rappel en parallèle, vous devez faire naître de nouvelles discussions pour l'aide d'un rappel de proxy qui utilise EM.defer comme ceci:

EM.run { 
    times = 0 

    work = proc { sleep 1 } 

    callback = proc { 
    sleep 1 
    EM.stop if (times += 1) >= 10 
    } 

    proxy_callback = proc { EM.defer callback } 

    10.times { EM.defer work, proxy_callback } 
} 

Cependant, vous pouvez rencontrer des problèmes avec cela si votre rappel est alors supposé exécuter du code dans la boucle d'événement, car il est exécuté dans un thread distinct et différé. Si cela se produit, déplacez le code de problème dans le rappel du proc proxy_callback.

EM.run { 
    times = 0 

    work = proc { sleep 1 } 

    callback = proc { 
    sleep 1 
    EM.stop_event_loop if (times += 1) >= 5 
    } 

    proxy_callback = proc { EM.defer callback, proc { "do_eventmachine_stuff" } } 

    10.times { EM.defer work, proxy_callback } 
} 

Cette version a couru en 3 secondes environ, ce qui représente 1 seconde de sommeil pour un fonctionnement en parallèle, 1 seconde de sommeil pour le rappel en parallèle et 1 seconde pour les frais généraux.

+0

Merci Ben! J'ai pris un peu plus loin votre exemple de proxy et créé une fonction de déblocage. Vous pouvez voir ma mise en œuvre ici: http://goo.gl/8kbc6y –