Je voudrais paralléliser mon programme Python afin qu'il puisse utiliser plusieurs processeurs sur la machine sur laquelle il fonctionne. Ma parallélisation est très simple, en ce sens que tous les "threads" parallèles de le programme est indépendant et écrit sa 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 s'exécute sur n'importe quelle version Python sur Mac, Linux et Windows. Compte tenu de ces contraintes, qui est la plus appropriée Python module pour l'implémentation? J'essaie de choisir entre le thread, le sous-processus et le multi-traitement, qui semblent tous fournir des fonctionnalités connexes.
Des réflexions à ce sujet? J'aimerais la solution la plus simple qui soit portable.
multiprocessing
est 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 module subprocess
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 libéré en code C appelé depuis 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. Côté portabilité, notez que multiprocessing
n'est disponible en standard que depuis Python 2.6 (une version pour certaines anciennes versions de Python existe, Mais c'est un excellent module!
Pour moi, c'est en fait assez simple:
subprocess
est pour exécuter d'autres exécutables --- c'est essentiellement un wrapper autour de os.fork()
et os.execve()
avec une certaine prise en charge de la plomberie en option (configuration de TUYAUX vers et depuis les sous-processus. De toute évidence, vous pourriez utiliser d'autres mécanismes de communication inter-processus (IPC), tels que des sockets ou de la mémoire partagée Posix ou SysV. Mais vous allez être limité vers les interfaces et les canaux IPC pris en charge par les programmes que vous appelez.
Généralement, on utilise n'importe quel subprocess
de manière synchrone --- en appelant simplement un utilitaire externe et en relisant sa sortie ou en attendant son achèvement (peut-être en lisant ses résultats dans un fichier temporaire, ou après qu'il les ait publiés dans une base de données).
Cependant, on peut engendrer des centaines de sous-processus et les interroger. Mon utilitaire préféré personnel classh fait exactement cela. Le plus gros inconvénient du module subprocess
est que la prise en charge des E/S est généralement bloquante. Il existe un brouillon PEP-3145 pour corriger cela dans une future version de Python 3.x et une alternative asyncproc (Avertissement qui mène directement à la télécharger, pas à aucune sorte de documentation ni README). J'ai également constaté qu'il est relativement facile d'importer simplement fcntl
et de manipuler directement les descripteurs de fichiers Popen
PIPE --- même si je ne sais pas si cela est portable sur des plates-formes non UNIX.
(Mise à jour: 7 août 2019: Python 3 Prise en charge des sous-processus ayncio: https://docs.python.org/3/library/asyncio-subprocess.html )
subprocess
n'a presque pas de support de gestion d'événements ... cependant vous pouvez utiliser le module signal
et des signaux UNIX/Linux classiques à l'ancienne --- tuant vos processus en douceur, pour ainsi dire.
multiprocessing
est pour exécuter des fonctions dans votre code (Python) existant avec prise en charge de communications plus flexibles entre cette famille de processus. En particulier, il est préférable de construire votre multiprocessing
IPC autour des objets Queue
du module lorsque cela est possible, mais vous pouvez également utiliser les objets Event
et diverses autres fonctionnalités ( dont certains sont vraisemblablement construits autour du support mmap
sur les plateformes où ce support est suffisant).
Le module multiprocessing
de Python est destiné à fournir des interfaces et des fonctionnalités trè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 exploite tous les efforts de verrouillage et de cohérence SMP à grain fin effectués par les développeurs de votre noyau de système d'exploitation.
threading
est pour une gamme assez étroite d'applications qui sont liées aux E/S (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 threads (avec mémoire centrale partagée) par rapport à la commutation de processus/contexte. Sous Linux, il s'agit presque de l'ensemble vide (les temps de commutation des processus Linux sont extrêmement proches de ses commutateurs de threads).
threading
souffre de deux inconvénients majeurs en Python .
L'une, 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 seront souffrir de la contention de verrouillage GIL.
Le plus gros problème, qui n'est pas spécifique à l'implémentation, est que les threads partagent la même mémoire, les 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 modèle multiprocessing
donne à chaque processus sa propre mémoire, des descripteurs de fichiers, etc. Un crash ou une exception non gérée dans l'un d'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 le débogage, l'isolement et la correction ou la résolution de problèmes similaires dans les threads.
threading
avec les principaux systèmes Python, tels que NumPy , peut souffrir considérablement moins de conflits GIL que la plupart de vos propres Python le ferait. C'est parce qu'ils ont été spécialement conçus pour le faire; les parties natives/binaires de NumPy, par exemple, libéreront le GIL lorsque cela est sûr).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 peuvent prendre d'assaut ma maison avec des fourches et des torches, Twisted fournit une multitâche coopérative événementielle dans n'importe quel (unique) processus.
Pour comprendre comment cela est possible, il faut lire les caractéristiques de select()
(qui peuvent être construites autour de select () ou poll () ou appels système OS similaires). 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 certain délai.
L'éveil de chacun de ces appels à select()
est un événement --- soit impliquant une entrée disponible (lisible) sur un certain nombre de sockets ou des descripteurs de fichier, soit un espace tampon devenant disponible sur certains autres descripteurs (inscriptibles) ou des sockets, certaines conditions exceptionnelles (paquets Push-out TCP 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 en boucle sur le gestionnaire "principal" résultant, lui permettant de distribuer les événements à vos gestionnaires.
Je pense personnellement au nom, Twisted comme évocateur du modèle de programmation ... depuis votre approche du problème doit être, dans un certain sens, "tordu" à 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?) Une reactor()
).
Les principaux défis liés à l'utilisation de Twisted impliquent de tordre votre esprit autour du modèle piloté par les événements et d'éviter également l'utilisation de bibliothèques de classes ou de kits d'outils qui ne sont pas écrits en co -opé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, sembleraient dupliquer des choses dans le Python bibliothèques standard.
Je pense qu'il est utile de comprendre Twisted au niveau conceptuel même si vous n'avez jamais l'intention de l'utiliser. Il peut donner un aperçu des performances, des conflits et de la gestion des événements dans votre gestion des threads, du multitraitement et même des sous-processus, ainsi que de tout traitement distribué que vous entreprenez.
( Remarque: Les versions plus récentes de Python 3.x incluent asyncio (E/S asynchrones) des fonctionnalités telles que async def, le décorateur @ async.coroutine et le mot-clé wait et rendement du futur support. Tous ces éléments sont à peu près similaires à Twisted du point de vue du processus (multitâche coopératif)). (Pour l'état actuel de la prise en charge Twisted pour Python 3, consultez: https://twistedmatrix.com/documents/current/core/howto/python3.html )
Pourtant, un autre domaine du traitement que vous n'avez pas demandé, 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 facile à utiliser est celui qui est le moins souvent considéré comme étant dans cet espace.
Il est presque trivial de construire un traitement distribué autour de Redis . L'ensemble du magasin de clés peut être utilisé pour stocker les unités de travail et les résultats, les listes Redis peuvent être utilisées comme objet de type Queue()
et le support PUB/SUB peut être utilisé pour une gestion semblable à Event
. Vous pouvez hacher vos clés et utiliser des valeurs, répliquées sur un cluster lâche 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 une mise à l'échelle au-delà de la capacité d'une instance unique pour coordonner vos employés et marshaling des données (marinées, JSON, BSON ou YAML) entre elles.
Bien sûr, alors que vous commencez à construire une solution à plus grande échelle et plus sophistiquée autour de Redis, vous réimplémentez de nombreuses fonctionnalités qui ont déjà été résolues en utilisant, Celery , Apache Spark et Hadoop , Zookeeper , etcd , Cassandra et ainsi de suite. Ceux-ci ont tous des modules pour Python accès à leurs services.
[Mise à jour: quelques ressources à considérer si vous envisagez Python pour des calculs intensifs 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 particulièrement accessibles et populaires de science et d'analyse des données].
Vous avez là toute la gamme d'alternatives de traitement pour Python, du simple thread, avec de simples appels synchrones aux sous-processus, des pools de sous-processus interrogés, du thread et du multi-traitement, du multitâche coopératif événementiel et du traitement distribué.
Dans un cas similaire, j'ai opté pour des processus séparés et le peu de communication nécessaire via la prise réseau. Il 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 fils python, au moins lors de l'utilisation de CPython, ne sont pas de vrais fils. Eh bien, ce sont des fils système natifs mais des modules C appelés depuis Python peut ou non libérer le GIL et autoriser les autres threads à s'exécuter lors de l'appel du code de blocage.
Pour utiliser plusieurs processeurs dans CPython, votre uniquement choix est le module multiprocessing
. CPython garde un verrou sur ses internes (le GIL ) ce qui empêche les threads sur d'autres processeurs de fonctionner en parallèle. Le module multiprocessing
crée de nouveaux processus (comme subprocess
) et gère la communication entre eux.
Décortiquez et laissez l'Unix faire votre travail:
utilisez iterpipes pour envelopper le sous-processus, puis:
INPUTS_FROM_YOU | xargs -n1 -0 -P NUM ./process #NUM processus parallèles
OR
Gnu Parallel servira également
Vous passez du temps avec GIL pendant que vous envoyez les garçons des coulisses faire votre travail multicœur.