web-dev-qa-db-fra.com

Enfiler dans Python

Quels sont les modules utilisés pour écrire des applications multithread en Python? Je connais les mécanismes de concurrence de base fournis par le langage et aussi Stackless Python , mais quelles sont leurs forces et leurs faiblesses respectives?

76
Jon

Par ordre croissant de complexité:

Utilisez le module de threading

Avantages:

  • Il est vraiment facile d'exécuter n'importe quelle fonction (toute appelable en fait) dans son propre thread.
  • Le partage de données n'est pas facile (les verrous ne sont jamais faciles :), du moins simple.

Les inconvénients:

  • Comme mentionné par Juergen Python ne peuvent pas réellement accéder simultanément à l'état dans l'interpréteur (il y a un gros verrou, le tristement célèbre Global Interpreter Lock .)) En pratique, cela signifie que les threads sont utiles pour les tâches liées aux E/S (mise en réseau, écriture sur disque, etc.), mais pas du tout utiles pour effectuer des calculs simultanés.

Utilisez le module multiprocessing

Dans le cas d'utilisation simple, cela ressemble exactement à l'utilisation de threading sauf que chaque tâche est exécutée dans son propre processus et non dans son propre thread. (Presque littéralement: si vous prenez l'exemple d'Eli , et remplacez threading par multiprocessing, Thread, avec Process et Queue (le module) avec multiprocessing.Queue, il devrait fonctionner correctement.)

Avantages:

  • Concurrence réelle pour toutes les tâches (pas de verrouillage d'interprète global).
  • S'adapte à plusieurs processeurs, peut même évoluer vers plusieurs machines.

Les inconvénients:

  • Les processus sont plus lents que les threads.
  • Le partage de données entre les processus est plus délicat qu'avec les threads.
  • La mémoire n'est pas implicitement partagée. Vous devez soit le partager explicitement, soit vous devez décaper des variables et les envoyer d'avant en arrière. C'est plus sûr, mais plus difficile. (Si cela compte de plus en plus, les développeurs de Python semblent pousser les gens dans cette direction.)

Utilisez un modèle d'événement, tel que Twisted

Avantages:

  • Vous obtenez un contrôle extrêmement fin sur la priorité, sur ce qui s'exécute quand.

Les inconvénients:

  • Même avec une bonne bibliothèque, la programmation asynchrone est généralement plus difficile que la programmation threadée, difficile à la fois pour comprendre ce qui est censé se produire et pour déboguer ce qui se passe réellement.

Dans tous les cas, je suppose que vous comprenez déjà bon nombre des problèmes liés au multitâche, en particulier le problème délicat de la façon de partager des données entre les tâches. Si, pour une raison quelconque, vous ne savez pas quand et comment utiliser les verrous et les conditions, vous devez commencer par ceux-ci. Le code multitâche est plein de subtilités et d'accrochages, et il est vraiment préférable d'avoir une bonne compréhension des concepts avant de commencer.

115
quark

Vous avez déjà obtenu une bonne variété de réponses, des "faux fils" jusqu'aux frameworks externes, mais je n'ai vu personne mentionner Queue.Queue - la "sauce secrète" du threading CPython.

Pour développer: tant que vous n'avez pas besoin de chevaucher un traitement lourd en Python pur (auquel cas vous avez besoin de multiprocessing - mais il est également livré avec sa propre implémentation Queue, donc vous pouvez avec quelques précautions nécessaires appliquer les conseils généraux que je donne ;-), le threading intégré de Python fera l'affaire ... mais il le fera beaucoup mieux si vous l'utilisez à bon escient, par exemple, comme suit.

"Oubliez" la mémoire partagée, soi-disant le principal avantage du threading par rapport au multitraitement - cela ne fonctionne pas bien, il ne évolue pas bien, n'a jamais, ne fonctionnera jamais. Utilisez la mémoire partagée uniquement pour les structures de données qui sont configurées une fois avant vous générez des sous-threads et jamais modifiés par la suite - pour tout le reste, créez un single thread responsable de cette ressource et communiquez avec ce thread via Queue.

Consacrez un thread spécialisé à chaque ressource que vous pensez normalement protéger par des verrous: une structure de données mutable ou un groupe cohésif de celle-ci, une connexion à un processus externe (une base de données, un serveur XMLRPC, etc.), un fichier externe, etc., etc. . Obtenez un petit pool de threads pour des tâches générales qui n'ont pas ou n'ont pas besoin d'une ressource dédiée de ce type - ne pas générer des threads au fur et à mesure des besoins, ou la surcharge de changement de thread sera vous submerger.

La communication entre deux threads se fait toujours via Queue.Queue - une forme de passage de message, la seule base saine pour le multiprocessing (en plus de la mémoire transactionnelle, qui est prometteuse mais pour laquelle je ne connais aucune implémentation digne de production sauf In Haskell).

Chaque thread dédié gérant une seule ressource (ou un petit ensemble cohérent de ressources) écoute les demandes sur une instance de Queue.Queue spécifique. Les threads dans un pool attendent sur une seule file d'attente partagée (la file d'attente est solidement threadsafe et ne sera pas vous échouera dans ce domaine).

Les threads qui ont juste besoin de mettre en file d'attente une demande sur une file d'attente (partagée ou dédiée) le font sans attendre les résultats et continuent. Les threads qui ont finalement besoin d'un résultat ou d'une confirmation pour une file d'attente de demande une paire (demande, réception de file d'attente) avec une instance de file d'attente qu'ils viennent de créer, et finalement, lorsque la réponse ou la confirmation est indispensable pour continuer, ils obtiennent (attente ) de leur file d'attente de réception. Assurez-vous que vous êtes prêt à obtenir des réponses aux erreurs ainsi que des réponses ou des confirmations réelles (les deferred de Twisted sont parfaits pour organiser ce type de réponse structurée, BTW!).

Vous pouvez également utiliser Queue pour "garer" des instances de ressources qui peuvent être utilisées par n'importe quel thread mais ne jamais être partagées entre plusieurs threads à la fois (connexions DB avec certains composants DBAPI, curseurs avec d'autres, etc.) - cela vous permet de vous détendre l'exigence de thread dédié en faveur d'une plus grande mise en commun (un thread de pool qui obtient de la file d'attente partagée une requête nécessitant une ressource pouvant être mise en file d'attente obtiendra cette ressource de la file d'attente appropriée, attendra si nécessaire, etc etc).

Twisted est en fait un bon moyen d'organiser ce menuet (ou danse carrée selon le cas), non seulement grâce aux différés, mais en raison de son architecture de base solide, solide et hautement évolutive: vous pouvez arranger les choses pour utiliser des threads ou des sous-processus uniquement lorsque vraiment garanti, tout en faisant la plupart des choses normalement considérées comme dignes d'un thread dans un seul thread événementiel.

Mais, je me rends compte que Twisted n'est pas pour tout le monde - l'approche "dédier ou mettre en commun des ressources, utiliser Queue dans le wazoo, ne jamais rien avoir besoin d'un verrou ou, Guido l'interdisent, toute procédure de synchronisation encore plus avancée, comme un sémaphore ou une condition", l'approche peut sera toujours utilisé même si vous ne pouvez pas vous concentrer sur les méthodologies basées sur les événements asynchrones, et fournira toujours plus de fiabilité et de performances que toute autre approche de threading largement applicable sur laquelle je suis tombé.

102
Alex Martelli

Cela dépend de ce que vous essayez de faire, mais je préfère simplement utiliser le module threading dans la bibliothèque standard, car il est très facile de prendre n'importe quelle fonction et de l'exécuter dans un thread séparé.

from threading import Thread

def f():
    ...

def g(arg1, arg2, arg3=None):
    ....

Thread(target=f).start()
Thread(target=g, args=[5, 6], kwargs={"arg3": 12}).start()

Etc. J'ai souvent une configuration producteur/consommateur utilisant une file d'attente synchronisée fournie par le module Queue

from Queue import Queue
from threading import Thread

q = Queue()
def consumer():
    while True:
        print sum(q.get())

def producer(data_source):
    for line in data_source:
        q.put( map(int, line.split()) )

Thread(target=producer, args=[SOME_INPUT_FILE_OR_SOMETHING]).start()
for i in range(10):
    Thread(target=consumer).start()
20
Eli Courtwright

En ce qui concerne Kamaelia, la réponse ci-dessus ne couvre pas vraiment l'avantage ici. L'approche de Kamaelia fournit une interface unifiée, qui n'est pas parfaite pragmatique, pour traiter les threads, les générateurs et les processus dans un seul système de concurrence.

Fondamentalement, il fournit une métaphore d'une chose en cours d'exécution qui a des boîtes de réception et des boîtes d'envoi. Vous envoyez des messages aux boîtes d'envoi et, lorsqu'ils sont connectés ensemble, les messages circulent des boîtes d'envoi vers les boîtes de réception. Cette métaphore/API reste la même que vous utilisiez des générateurs, des threads ou des processus, ou que vous parliez à d'autres systèmes.

La partie "non parfaite" est due au fait que le sucre syntaxique n'est pas encore ajouté pour les boîtes de réception et les boîtes d'envoi (bien que cela soit en cours de discussion) - l'accent est mis sur la sécurité/l'utilisabilité du système.

En prenant l'exemple du producteur consommateur en utilisant le filetage nu ci-dessus, cela devient ceci en Kamaelia:

Pipeline(Producer(), Consumer() )

Dans cet exemple, peu importe qu'il s'agisse de composants filetés ou non, la seule différence entre eux du point de vue de l'utilisation est la classe de base du composant. Les composants du générateur communiquent à l'aide de listes, les composants filetés à l'aide de Queue.Queues et les processus basés sur os.pipes.

La raison derrière cette approche est cependant de rendre plus difficile le débogage des bogues. Dans le filetage - ou dans toute concurrence de mémoire partagée que vous avez, le problème numéro un auquel vous êtes confronté est la mise à jour accidentelle des données partagées. En utilisant la transmission de messages, vous éliminez une classe de bogues.

Si vous utilisez des threads nus et des verrous partout, vous travaillez généralement sur l'hypothèse que lorsque vous écrivez du code, vous ne ferez aucune erreur. Bien que nous aspirions tous à cela, il est très rare que cela se produise. En regroupant le comportement de verrouillage en un seul endroit, vous simplifiez les problèmes. (Les gestionnaires de contexte aident, mais ne facilitent pas les mises à jour accidentelles en dehors du gestionnaire de contexte)

Évidemment, tous les morceaux de code ne peuvent pas être écrits sous forme de passage de messages et de style partagé.C'est pourquoi Kamaelia possède également une simple mémoire transactionnelle logicielle (STM), ce qui est une idée vraiment soignée avec un nom méchant - c'est plus comme un contrôle de version pour les variables - c'est-à-dire consultez certaines variables, mettez-les à jour et validez. Si vous obtenez un affrontement, vous rincez et répétez.

Liens pertinents:

Quoi qu'il en soit, j'espère que c'est une réponse utile. FWIW, la raison principale de la configuration de Kamaelia est de rendre la concurrence plus sûre et plus facile à utiliser dans les systèmes python, sans que la queue ne remue le chien (c'est-à-dire le grand seau de composants)

Je peux comprendre pourquoi l'autre réponse de Kamaelia a été modifiée, car même pour moi, cela ressemble plus à une annonce qu'à une réponse. En tant qu'auteur de Kamaelia, c'est agréable de voir de l'enthousiasme, mais j'espère que cela contient un contenu un peu plus pertinent :-)

Et c'est ma façon de dire, veuillez prendre la mise en garde que cette réponse est par définition biaisée, mais pour moi, le but de Kamaelia est d'essayer d'envelopper ce qui est la meilleure pratique de l'OMI. Je suggère d'essayer quelques systèmes et de voir celui qui fonctionne pour vous. (aussi si cela ne convient pas au débordement de pile, désolé - je suis nouveau sur ce forum :-)

6
Michael Sparks

J'utiliserais les Microthreads (Tasklets) de Stackless Python, si je devais utiliser des threads.

Tout un jeu en ligne (massivement multijoueur) est construit autour de Stackless et de son principe de multithreading - car l'original est juste de ralentir pour la propriété massivement multijoueur du jeu.

Les threads en CPython sont largement déconseillés. L'une des raisons est le GIL - un verrou d'interpréteur global - qui sérialise le threading pour de nombreuses parties de l'exécution. Mon expérience est qu'il est vraiment difficile de créer des applications rapides de cette façon. Mes exemples de codages étaient tous plus lents avec le filetage - avec un seul noyau (mais de nombreuses attentes d'entrée auraient dû rendre possible une amélioration des performances).

Avec CPython, utilisez plutôt des processus séparés si possible.

3
Juergen

Si vous voulez vraiment vous salir les mains, vous pouvez essayer en utilisant des générateurs pour simuler des coroutines . Ce n'est probablement pas le plus efficace en termes de travail, mais les coroutines vous offrent un contrôle très fin du multitâche coopératif plutôt que préventif multitâche, vous trouverez ailleurs.

Un avantage que vous constaterez est que, dans l'ensemble, vous n'aurez pas besoin de verrous ou de mutex lors de l'utilisation du multitâche coopératif, mais l'avantage le plus important pour moi était la vitesse de commutation presque nulle entre les "threads". Bien sûr, Stackless Python est également très bon pour cela; et puis il y a Erlang, si ce n'est pas le cas pour être Python.

Le principal inconvénient du multitâche coopératif est probablement le manque général de solution pour bloquer les E/S. Et dans les coroutines truquées, vous rencontrerez également le problème selon lequel vous ne pouvez pas changer de "threads" depuis autre chose que le niveau supérieur de la pile dans un thread.

Après avoir créé une application encore légèrement complexe avec de fausses coroutines, vous commencerez vraiment à apprécier le travail qui va dans la planification des processus au niveau du système d'exploitation.

2
Mark Rushakoff