web-dev-qa-db-fra.com

Quel genre de problèmes (le cas échéant) y aurait-il à combiner asyncio et multitraitement?

Comme presque tout le monde le sait quand ils examinent pour la première fois le filetage en Python, il y a le GIL qui rend la vie misérable pour les personnes qui veulent réellement faire du traitement en parallèle - ou du moins lui donner une chance.

Je cherche actuellement à implémenter quelque chose comme le modèle Reactor. En fait, je veux écouter les connexions de socket entrantes sur un thread, et lorsque quelqu'un essaie de se connecter, acceptez cette connexion et transmettez-la à un autre thread pour le traitement.

Je ne sais pas (encore) à quel type de charge je pourrais faire face. Je sais qu'il existe actuellement un plafond de 2 Mo pour les messages entrants. Théoriquement, nous pourrions obtenir des milliers par seconde (même si je ne sais pas si nous avons pratiquement vu quelque chose comme ça). Le temps passé à traiter un message n'est pas terriblement important, bien que plus rapide serait évidemment préférable.

J'examinais le modèle Reactor et j'ai développé un petit exemple en utilisant la bibliothèque multiprocessing qui (au moins lors des tests) semble fonctionner très bien. Cependant, maintenant/bientôt, nous aurons la bibliothèque asyncio disponible, qui gérerait la boucle d'événements pour moi.

Y a-t-il quelque chose qui pourrait me mordre en combinant asyncio et multiprocessing?

53
Wayne Werner

Vous devriez pouvoir combiner en toute sécurité asyncio et multiprocessing sans trop de problèmes, bien que vous ne devriez pas utiliser multiprocessing directement. Le péché cardinal de asyncio (et de tout autre framework asynchrone basé sur la boucle d'événements) bloque la boucle d'événements. Si vous essayez d'utiliser multiprocessing directement, chaque fois que vous bloquez pour attendre un processus enfant, vous allez bloquer la boucle d'événements. De toute évidence, c'est mauvais.

La façon la plus simple d'éviter cela est d'utiliser BaseEventLoop.run_in_executor pour exécuter une fonction dans un concurrent.futures.ProcessPoolExecutor . ProcessPoolExecutor est un pool de processus implémenté à l'aide de multiprocessing.Process, mais asyncio a une prise en charge intégrée pour l'exécution d'une fonction sans bloquer la boucle d'événements. Voici un exemple simple:

import time
import asyncio
from concurrent.futures import ProcessPoolExecutor

def blocking_func(x):
   time.sleep(x) # Pretend this is expensive calculations
   return x * 5

@asyncio.coroutine
def main():
    #pool = multiprocessing.Pool()
    #out = pool.apply(blocking_func, args=(10,)) # This blocks the event loop.
    executor = ProcessPoolExecutor()
    out = yield from loop.run_in_executor(executor, blocking_func, 10)  # This does not
    print(out)

if __== "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Pour la majorité des cas, cette fonction seule est suffisante. Si vous avez besoin d'autres constructions de multiprocessing, comme Queue, Event, Manager, etc., il existe une bibliothèque tierce appelée - aioprocessing (divulgation complète: je l'ai écrit), qui fournit des versions compatibles avec asyncio de toutes les structures de données multiprocessing. Voici un exemple de démonstration qui:

import time
import asyncio
import aioprocessing
import multiprocessing

def func(queue, event, lock, items):
    with lock:
        event.set()
        for item in items:
            time.sleep(3)
            queue.put(item+5)
    queue.close()

@asyncio.coroutine
def example(queue, event, lock):
    l = [1,2,3,4,5]
    p = aioprocessing.AioProcess(target=func, args=(queue, event, lock, l)) 
    p.start()
    while True:
        result = yield from queue.coro_get()
        if result is None:
            break
        print("Got result {}".format(result))
    yield from p.coro_join()

@asyncio.coroutine
def example2(queue, event, lock):
    yield from event.coro_wait()
    with (yield from lock):
        yield from queue.coro_put(78)
        yield from queue.coro_put(None) # Shut down the worker

if __== "__main__":
    loop = asyncio.get_event_loop()
    queue = aioprocessing.AioQueue()
    lock = aioprocessing.AioLock()
    event = aioprocessing.AioEvent()
    tasks = [ 
        asyncio.async(example(queue, event, lock)),
        asyncio.async(example2(queue, event, lock)),
    ]   
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()
62
dano

Oui, il y a pas mal de morceaux qui peuvent (ou non) vous mordre.

  • Lorsque vous exécutez quelque chose comme asyncio, il s'attend à s'exécuter sur un thread ou un processus. Cela ne fonctionne pas (en soi) avec le traitement parallèle. Vous devez en quelque sorte distribuer le travail tout en laissant les opérations IO (en particulier celles sur les sockets) dans un seul thread/processus.
  • Bien que votre idée de transférer des connexions individuelles à un processus de gestionnaire différent soit agréable, elle est difficile à mettre en œuvre. Le premier obstacle est que vous avez besoin d'un moyen de retirer la connexion de asyncio sans la fermer. L'obstacle suivant est que vous ne pouvez pas simplement envoyer un descripteur de fichier à un processus différent à moins d'utiliser du code spécifique à la plate-forme (probablement Linux) à partir d'une extension C.
  • Notez que le module multiprocessing est connu pour créer un certain nombre de threads pour la communication. La plupart du temps, lorsque vous utilisez des structures de communication (telles que Queues), un thread est généré. Malheureusement, ces fils ne sont pas complètement invisibles. Par exemple, ils peuvent échouer à démolir proprement (lorsque vous avez l'intention de terminer votre programme), mais en fonction de leur nombre, l'utilisation des ressources peut être perceptible en elle-même.

Si vous avez vraiment l'intention de gérer des connexions individuelles dans des processus individuels, je suggère d'examiner différentes approches. Par exemple, vous pouvez mettre un socket en mode écoute, puis accepter simultanément les connexions de plusieurs processus de travail en parallèle. Une fois qu'un travailleur a terminé de traiter une demande, il peut accepter la connexion suivante, vous utilisez donc toujours moins de ressources que de bifurquer un processus pour chaque connexion. Spamassassin et Apache (mpm prefork) peuvent utiliser ce modèle de travail par exemple. Cela pourrait se révéler plus facile et plus robuste selon votre cas d'utilisation. Plus précisément, vous pouvez faire mourir vos employés après avoir servi un nombre configuré de demandes et être réapparu par un processus maître, éliminant ainsi la plupart des effets négatifs des fuites de mémoire.

5
Helmut Grohne

Voir PEP 3156, en particulier la section sur l'interaction des threads:

http://www.python.org/dev/peps/pep-3156/#thread-interaction

Cela documente clairement les nouvelles méthodes asyncio que vous pourriez utiliser, y compris run_in_executor (). Notez que l'exécuteur est défini dans concurrent.futures, je vous suggère également d'y jeter un œil.

1
Glenn