Python: Multithreading VS Multiprocessing
Introduction
Les modules de threading et de multiprocessing en python visent à faire la même chose, c’est-à-dire à faire plusieurs choses en même temps, mais la façon dont le module de threading et le module de multiprocessing s’y prennent est très différente.
C’est pourquoi une définition générale s’impose:
Process: Est une instance d’un programme qui tourne dans un ordinateur (une machine)
- Propriété des ressources: Espace d’adressage virtuel pour contenir l’image de processus, y compris le programme, les données, la pile et les attributs
- L’exécution d’un processus suit un chemin à travers le programme
- Commutateur de processus: Une opération coûteuse (lourde) en raison de la nécessité de sauvegarder les données de contrôle et d’enregistrer le contenu
Thread: Est une unité de répartition d’une œuvre dans un processus
- Interruptible: le processus peut se tourner vers un autre thread
- Tous les fils d’un processus partagent le code et les segments de données
- Le commutateur de thread est généralement moins coûteux que le commutateur de processus
Un exemple concret: Supposons qu’on est entrain de créer une GUI (interface graphique) et dans cette dernière on aura besoin de cliquer sur un bouton pour afficher un text dans une zone précise de cette interface. De ce fait:
- En utilisant le principe de
multiprocessing
: Le programme va créer un nouveau process, une nouvelle interface, et affichera le texte. C’est à dire, on va créer un nouveau espace d’adressage virtuel en chargeant une copie du code, les données etc.. en lançant la nouvelle fonctionnalité d’affichage du text après le clic sur le bouton. Ceci dit, le nouveau text ne sera visible que dans la nouvelle instance du programme. - En utilisant le principe de
threading/Multithreading
: Le programme va créer un nouveau thread au sein du process principal de l’interface graphique et lancera la fonction d’affichage du texte. Ceci dit, le thread sera lancé comme une nouvelle unité de calcul au sein du process qui traite l’interface graphique et tout le code, les données, etc.. du programme principale seront accéssible par tous les threads lancés par ce process en question.
Différences entre Threading et Multiprocessing
Threading:
- Un nouveau thread est généré dans le processus existant
- Le démarrage d’un thread est plus rapide que le démarrage d’un processus
- La mémoire est partagée entre tous les threads
- Des mutex souvent nécessaires pour contrôler l’accès aux données partagées
- Un GIL (Global Interpreter Lock) pour tous les threads (Important)
- Il ne peut pas faire les choses en parallèle mais il peut faire les choses simultanément (Concurrency: Taches concurrentes), c’est-à-dire qu’il peut faire des allers-retours entre les choses quand il en a le temps. (Lire en dessous!)
Multiprocessing:
- Un nouveau processus est lancé indépendamment du premier processus
- Le démarrage d’un processus est plus lent que le démarrage d’un thread
- La mémoire n’est pas partagée entre les processus (c’est-à-dire plus de mémoire impliquée lors du démarrage d’un processus par rapport à un thread bien que le processus enfant ait accès à la mémoire du processus parent). (Lire en dessous!)
- Les mutex ne sont pas nécessaires (sauf si le threading dans le nouveau processus, c’est-à-dire que le processus enfant est en train de threader)
- Un GIL (Global Interpreter Lock) pour chaque processus
A noter:
Concurrency: Taches concurrentes
A la première vue on pourra dire que les taches concurrentes (Threading et Couroutines) ne tournent pas en parallère et ceci est vrai en absolue. Car, la définition la plus banale de ce concept dit que que les threads peuvent faire des allers-retours entre les taches quand ils auront le temps. Cependant, en diminuant la fréquence d’altération d’une tache vers une autre on pourra aboutir à un processus qui tendera vers des processus qui tournent en parallèle.
Deux processus peuvent-ils partager le même segment de mémoire partagée?
Oui et non. Généralement, avec les systèmes d’exploitation modernes, lorsqu’un autre processus est dérivé du premier, ils partagent le même espace mémoire avec un jeu de copie en écriture sur toutes les pages. Toutes les mises à jour apportées à l’une des pages de mémoire en lecture-écriture entraînent une copie de la page, il y aura donc deux copies et la page de mémoire ne sera plus partagée entre le processus parent et enfant. Cela signifie que seules les pages en lecture seule ou les pages qui n’ont pas été écrites seront partagées.
Si un processus n’a pas été issu d’un autre (aucun fork), il ne partage généralement pas de mémoire. Une exception est que si vous exécutez deux instances du même programme, elles peuvent partager du code et peut-être même des segments de données statiques, mais aucune autre page ne sera partagée.
Il existe également des appels de mappage de mémoire spécifiques pour partager le même segment de mémoire. L’appel indique si la carte est en lecture seule ou en lecture-écriture. La façon de procéder est très dépendante du système d’exploitation.
Deux threads peuvent-ils partager la même mémoire partagée?
Certainement. En général, toute la mémoire à l’intérieur d’un processus multithread est “partagée” par tous les threads. C’est généralement la définition des threads en ce sens qu’ils s’exécutent tous dans le même espace mémoire.
Les threads ont également la complexité supplémentaire d’avoir des segments de mémoire mis en cache dans la mémoire haute vitesse liés au processeur / noyau. Cette mémoire en cache n’est pas partagée et les mises à jour des pages de mémoire sont vidées dans le stockage central en fonction des opérations de synchronisation. Et c’est là où le GIL (Global Interpreter Lock) de Python intervient.
Assez de théories :D faisons un peu de code !
Multithreading avec/sans Mutex:
Multiprocessing avec Mutex: Notez dans cet exemple le fait qu’on a partagé une liste et des chiffres entre les différents process en utilisant des Mutex.
Multiprocessing avec une Pool: Notez ici le fait que les processus sont indépendant les uns des autres.
Coroutines en utilisant Async/Await:
Coroutines en utilisant ThreadPoolExecutor:
Coroutines en utilisant des Mutex:
Avant de finir: Une question qui se pose toujours: Quand dois-je utiliser le Multithreading
? Quand dois-je utiliser le Multiprocessing
? et Quand dois-je utiliser les Coroutines
?
En gros:
- Multiprocessing: Quand on cherche de paralléliser des processus liés à la consommation des unités de calcul (CPU bound)
- Multithreading/Concurrency: Quand on cherche de “paralléliser”/faire des taches concurrentes des processus liés à IO (Input Output) (IO bound)
- Coroutines: Quand on le peut sinon on utilise le
Multithreading
.
Commentaires