choisir entre sous-processus, multi-traitement et thread en Python?


110

Je voudrais paralléliser mon programme Python afin qu'il puisse utiliser plusieurs processeurs sur la machine sur laquelle il s'exécute. Ma parallélisation est très simple, en ce que tous les "threads" parallèles du programme sont indépendants et écrivent leur sortie dans des fichiers séparés. Je n'ai pas besoin des threads pour échanger des informations mais il est impératif que je sache quand les threads se terminent car certaines étapes de mon pipeline dépendent de leur sortie.

La portabilité est importante, dans la mesure où j'aimerais que cela fonctionne sur n'importe quelle version de Python sur Mac, Linux et Windows. Compte tenu de ces contraintes, quel est le module Python le plus approprié pour l'implémenter? J'essaie de choisir entre le thread, le sous-processus et le multitraitement, qui semblent tous fournir des fonctionnalités connexes.

Des pensées à ce sujet? J'aimerais la solution la plus simple et portable.


En relation: stackoverflow.com/questions/1743293/… (lisez ma réponse ici pour voir pourquoi les threads ne sont pas un démarreur pour le code pur-Python)

1
"N'importe quelle version de Python" est BEAUCOUP trop vague. Python 2.3? 1 fois? 3.x? C'est simplement une condition impossible à satisfaire.
detly le

Réponses:


64

multiprocessingest un excellent module de type couteau suisse. C'est plus général que les threads, car vous pouvez même effectuer des calculs à distance. C'est donc le module que je vous suggère d'utiliser.

Le subprocessmodule vous permettrait également de lancer plusieurs processus, mais je l'ai trouvé moins pratique à utiliser que le nouveau module multitraitement.

Les threads sont notoirement subtils et, avec CPython, vous êtes souvent limité à un noyau, avec eux (même si, comme indiqué dans l'un des commentaires, le Global Interpreter Lock (GIL) peut être publié en code C appelé à partir du code Python) .

Je pense que la plupart des fonctions des trois modules que vous citez peuvent être utilisées de manière indépendante de la plate-forme. Du côté de la portabilité, notez que ce multiprocessingn'est en standard que depuis Python 2.6 (une version pour certaines anciennes versions de Python existe cependant). Mais c'est un excellent module!


1
pour une affectation, je viens d'utiliser le module "multiprocessing" et sa méthode pool.map (). part de gâteau !
kmonsoor

Une chose comme le céleri est-elle également envisagée? Pourquoi est-ce ou non?
user3245268

Pour autant que je sache, Celery est plus impliqué (vous devez installer un courtier de messages), mais c'est une option qui devrait probablement être envisagée, en fonction du problème à résoudre.
Eric O Lebigot

186

Pour moi, c'est en fait assez simple:

L' option de sous-processus :

subprocessest pour exécuter d'autres exécutables --- c'est fondamentalement un wrapper autour os.fork()et os.execve()avec un certain support pour la plomberie facultative (configuration des PIPE vers et depuis les sous-processus. Évidemment, vous pouvez d'autres mécanismes de communication inter-processus (IPC), tels que les sockets, ou Posix ou Mémoire partagée SysV. Mais vous serez limité aux interfaces et aux canaux IPC pris en charge par les programmes que vous appelez.

Généralement, on utilise n'importe quel subprocesssynchrone - il suffit d'appeler un utilitaire externe et de lire sa sortie ou d'attendre son achèvement (peut-être en lisant ses résultats à partir d'un fichier temporaire, ou après les avoir publiés dans une base de données).

Cependant, on peut engendrer des centaines de sous-processus et les interroger. C'est exactement ce que fait ma propre classe utilitaire préférée . Le plus gros inconvénient du subprocessmodule est que la prise en charge des E / S est généralement bloquante. Il y a un projet de PEP-3145 pour corriger cela dans une version future de Python 3.x et une alternative asyncproc (Avertissement qui mène directement au téléchargement, pas à une sorte de documentation ni à README). J'ai également trouvé qu'il est relativement facile d'importer fcntlet de manipuler Popendirectement vos descripteurs de fichiers PIPE - même si je ne sais pas si cela est portable sur des plates-formes non-UNIX.

(Mise à jour: 7 août 2019: prise en charge de Python 3 pour les sous-processus ayncio : sous-processus asyncio )

subprocess n'a presque pas de support de gestion d'événements ... bien que vous puissiez utiliser le signalmodule et les signaux UNIX / Linux à l'ancienne - en tuant vos processus doucement, pour ainsi dire.

L' option multitraitement :

multiprocessingest pour exécuter des fonctions dans votre code (Python) existant avec la prise en charge de communications plus flexibles entre cette famille de processus. En particulier, il est préférable de construire votre multiprocessingIPC autour des Queueobjets du module lorsque cela est possible, mais vous pouvez également utiliser des Eventobjets et diverses autres fonctionnalités (dont certaines sont vraisemblablement construites autour du mmapsupport sur les plates-formes où ce support est suffisant).

Le multiprocessingmodule de Python est destiné à fournir des interfaces et des fonctionnalités très similaires threading tout en permettant à CPython de faire évoluer votre traitement entre plusieurs processeurs / cœurs malgré le GIL (Global Interpreter Lock). Il tire parti de tous les efforts de verrouillage et de cohérence SMP à granularité fine réalisés par les développeurs de votre noyau OS.

L' option de filetage :

threadingest destiné à une gamme assez restreinte d'applications qui sont liées aux E / S (n'ont pas besoin d'évoluer sur plusieurs cœurs de processeur) et qui bénéficient de la latence extrêmement faible et de la surcharge de commutation de la commutation de thread (avec mémoire centrale partagée) par rapport au processus / changement de contexte. Sur Linux, c'est presque l'ensemble vide (les temps de changement de processus Linux sont extrêmement proches de ses commutateurs de thread).

threadingsouffre de deux inconvénients majeurs en Python .

L'un, bien sûr, est spécifique à l'implémentation --- affectant principalement CPython. C'est le GIL. Pour la plupart, la plupart des programmes CPython ne bénéficieront pas de la disponibilité de plus de deux processeurs (cœurs) et souvent les performances souffriront du conflit de verrouillage GIL.

Le problème le plus important qui n'est pas spécifique à l'implémentation est que les threads partagent la même mémoire, les mêmes gestionnaires de signaux, les descripteurs de fichiers et certaines autres ressources du système d'exploitation. Ainsi, le programmeur doit être extrêmement prudent sur le verrouillage des objets, la gestion des exceptions et d'autres aspects de leur code qui sont à la fois subtils et qui peuvent tuer, bloquer ou bloquer tout le processus (suite de threads).

Par comparaison, le multiprocessingmodèle donne à chaque processus sa propre mémoire, ses propres descripteurs de fichiers, etc. Un plantage ou une exception non gérée dans l'un d'entre eux ne fera que tuer cette ressource et gérer de manière robuste la disparition d'un processus enfant ou frère peut être considérablement plus facile que de déboguer, isoler et résoudre ou contourner des problèmes similaires dans les threads.

  • (Remarque: l'utilisation de threadingavec les principaux systèmes Python, tels que NumPy , peut souffrir beaucoup moins de conflits GIL que la plupart de votre propre code Python. C'est parce qu'ils ont été spécialement conçus pour le faire; les parties natives / binaires de NumPy, par exemple, publiera le GIL quand c'est sûr).

L' option tordue :

Il convient également de noter que Twisted offre une autre alternative à la fois élégante et très difficile à comprendre . Fondamentalement, au risque de trop simplifier au point où les fans de Twisted pourraient prendre d'assaut ma maison avec des fourches et des torches, Twisted fournit un multitâche coopératif axé sur les événements dans n'importe quel processus (unique).

Pour comprendre comment cela est possible, il faut lire les fonctionnalités de select()(qui peuvent être construites autour de select () ou poll () ou des appels système similaires du système d'exploitation). Fondamentalement, tout est motivé par la possibilité de demander au système d'exploitation de se mettre en veille en attendant toute activité sur une liste de descripteurs de fichiers ou un délai d'expiration.

Le réveil de chacun de ces appels à select()est un événement --- soit un événement impliquant une entrée disponible (lisible) sur un certain nombre de sockets ou de descripteurs de fichier, soit l'espace tampon devenant disponible sur certains autres descripteurs ou sockets (inscriptibles), certaines conditions exceptionnelles (TCP paquets PUSH hors bande, par exemple), ou un TIMEOUT.

Ainsi, le modèle de programmation Twisted est construit autour de la gestion de ces événements puis de la boucle sur le gestionnaire "principal" résultant, lui permettant de distribuer les événements à vos gestionnaires.

Personnellement, je pense au nom Twisted comme évocateur du modèle de programmation ... puisque votre approche du problème doit être, dans un certain sens, "tordue" à l'envers. Plutôt que de concevoir votre programme comme une série d'opérations sur les données d'entrée et les sorties ou les résultats, vous écrivez votre programme en tant que service ou démon et définissez comment il réagit à divers événements. (En fait, la "boucle principale" principale d'un programme Twisted est (généralement? Toujours?) A reactor()).

Les principaux défis de l'utilisation de Twisted impliquent de tordre votre esprit autour du modèle événementiel et également d'éviter l'utilisation de bibliothèques de classes ou de boîtes à outils qui ne sont pas écrites pour coopérer dans le cadre Twisted. C'est pourquoi Twisted fournit ses propres modules pour la gestion du protocole SSH, pour les curses, et ses propres fonctions de sous-processus / Popen, et de nombreux autres modules et gestionnaires de protocole qui, à première vue, semblent dupliquer des éléments dans les bibliothèques standard Python.

Je pense qu'il est utile de comprendre Twisted à un niveau conceptuel même si vous n'avez jamais l'intention de l'utiliser. Il peut donner des informations sur les performances, les conflits et la gestion des événements dans votre threading, votre multitraitement et même la gestion de sous-processus ainsi que tout traitement distribué que vous entreprenez.

( Note: Les nouvelles versions de Python 3.x sont notamment asyncio (E / S asynchrones) caractéristiques telles que async def , le @ async.coroutine décorateur, et le await mot - clé, et le rendement du futur soutien Tous ces éléments sont à peu près semblables à. Tordu du point de vue du processus (multitâche coopératif)). (Pour connaître l'état actuel de la prise en charge de Twisted pour Python 3, consultez: https://twistedmatrix.com/documents/current/core/howto/python3.html )

L' option distribuée :

Un autre domaine du traitement sur lequel vous n'avez pas posé de questions, mais qui mérite d'être considéré, est celui du traitement distribué . Il existe de nombreux outils et frameworks Python pour le traitement distribué et le calcul parallèle. Personnellement, je pense que le plus simple à utiliser est celui qui est le moins souvent considéré comme étant dans cet espace.

Il est presque trivial de créer un traitement distribué autour de Redis . L'ensemble du magasin de clés peut être utilisé pour stocker des unités de travail et des résultats, les listes Redis peuvent être utilisées comme Queue()un objet similaire et le support PUB / SUB peut être utilisé pour une Eventgestion similaire. Vous pouvez hacher vos clés et utiliser des valeurs, répliquées sur un cluster libre d'instances Redis, pour stocker la topologie et les mappages de jetons de hachage afin de fournir un hachage et un basculement cohérents pour évoluer au-delà de la capacité d'une seule instance pour coordonner vos travailleurs. et rassembler les données (pickled, JSON, BSON ou YAML) parmi eux.

Bien sûr, alors que vous commencez à créer une solution à plus grande échelle et plus sophistiquée autour de Redis, vous réinstallez de nombreuses fonctionnalités qui ont déjà été résolues à l'aide de Celery , Apache Spark et Hadoop , Zookeeper , etcd , Cassandra , etc. Ceux-ci ont tous des modules pour l'accès Python à leurs services.

[Mise à jour: quelques ressources à prendre en compte si vous envisagez d' utiliser Python pour un calcul intensif sur les systèmes distribués: IPython Parallel et PySpark . Bien qu'il s'agisse de systèmes informatiques distribués à usage général, ce sont des sous-systèmes de science et d'analyse des données particulièrement accessibles et populaires].

Conclusion

Vous avez là toute une gamme d'alternatives de traitement pour Python, allant du simple thread, avec de simples appels synchrones à des sous-processus, des pools de sous-processus interrogés, des threads et du multitraitement, le multitâche coopératif basé sur les événements et le traitement distribué.


1
Il est cependant difficile d'utiliser le multitraitement avec les classes / POO.
Tjorriemorrie

2
@Tjorriemorrie: Je suppose que vous voulez dire qu'il est difficile d'envoyer des appels de méthode à des instances d'objets qui pourraient être dans d'autres processus. Je suggère que c'est le même problème que vous auriez avec les threads, mais plus facilement visible (plutôt que d'être fragile et soumis à des conditions de course obscures). Je pense que l'approche recommandée serait de faire en sorte que toutes ces expéditions se produisent via des objets Queue, qui fonctionnent à un seul thread, à plusieurs threads et à travers les processus. (Avec une implémentation Redis ou Celery Queue, même sur un cluster de nœuds)
Jim Dennis

2
C'est une très bonne réponse. J'aurais aimé que ce soit dans l'introduction à la concurrence dans la documentation Python3.
root-11

1
@ root-11 vous pouvez le proposer aux responsables du document; Je l'ai publié ici pour une utilisation gratuite. Vous et eux sont invités à l'utiliser, en tout ou en partie.
Jim Dennis

"Pour moi, c'est en fait assez simple:" J'adore. merci beaucoup
jerome

5

Dans un cas similaire, j'ai opté pour des processus séparés et le petit peu de prise réseau nécessaire pour la communication. C'est très portable et assez simple à faire en utilisant python, mais probablement pas le plus simple (dans mon cas j'avais aussi une autre contrainte: la communication avec d'autres processus écrits en C ++).

Dans votre cas, j'opterais probablement pour le multiprocessus, car les threads python, du moins lors de l'utilisation de CPython, ne sont pas de vrais threads. Eh bien, ce sont des threads système natifs, mais les modules C appelés à partir de Python peuvent ou non libérer le GIL et permettre à d'autres threads de s'exécuter lors de l'appel de code de blocage.


4

Pour utiliser plusieurs processeurs dans CPython, votre seul choix est le multiprocessingmodule. CPython garde un verrou sur ses internes (le GIL ) qui empêche les threads sur d'autres processeurs de fonctionner en parallèle. Le multiprocessingmodule crée de nouveaux processus (comme subprocess) et gère la communication entre eux.


5
Ce n'est pas tout à fait vrai, AFAIK vous pouvez publier le GIL en utilisant l'API C, et il existe d'autres implémentations de Python telles que IronPython ou Jython qui ne souffrent pas de telles limitations. Je n'ai pas voté contre.
Bastien Léonard

1

Shell out et laissez l'Unix faire votre travail:

utilisez iterpipes pour envelopper le sous-processus, puis:

Depuis le site de Ted Ziuba

INPUTS_FROM_YOU | xargs -n1 -0 -P NUM ./process #NUM processus parallèles

OU

Gnu Parallel servira également

Vous passez du temps avec GIL pendant que vous envoyez les garçons des coulisses faire votre travail multicœur.


6
"La portabilité est importante, dans la mesure où j'aimerais que cela fonctionne sur n'importe quelle version de Python sur Mac, Linux et Windows."
detly le

Avec cette solution, pouvez-vous interagir à plusieurs reprises avec le travail? Vous pouvez le faire en multitraitement, mais je ne le pense pas en sous-processus.
abalter
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.