Contactez-nous

Programmation concurrente et parallèle

Découvrez la programmation concurrente et parallèle en Python. Apprenez le multithreading (module threading), le multiprocessing (module multiprocessing), le Global Interpreter Lock (GIL), et la programmation asynchrone avec asyncio. Optimisez les perform

Introduction au multithreading (module `threading`) : exécutez plusieurs tâches "en même temps"

La programmation concurrente vous permet d'exécuter plusieurs tâches "en même temps", en tirant parti des temps d'attente (par exemple, lorsqu'une tâche attend une réponse d'un serveur ou une entrée de l'utilisateur). La programmation parallèle vous permet d'exécuter plusieurs tâches réellement en même temps, en utilisant plusieurs coeurs de processeur.

En Python, vous pouvez faire de la programmation concurrente avec le module `threading`, qui vous permet de créer et de gérer des threads (fils d'exécution). Un thread est une séquence d'instructions qui s'exécute indépendamment des autres threads, au sein du même processus.

Vous créez un thread en créant un objet `Thread` (du module `threading`), en lui passant une fonction cible (la fonction que le thread exécutera), et en appelant la méthode `start()` du thread. Le thread s'exécute alors en parallèle du thread principal (le thread qui a créé le thread).

Vous pouvez attendre qu'un thread se termine en appelant sa méthode `join()`. Vous pouvez également utiliser des verrous (locks), des sémaphores, des conditions et d'autres primitives de synchronisation (du module `threading`) pour coordonner l'exécution de plusieurs threads et éviter les problèmes de concurrence (comme les accès concurrents à des données partagées).

Nous verrons comment utiliser le module `threading`, comment créer et démarrer des threads, comment attendre qu'ils se terminent, et comment utiliser des primitives de synchronisation.

Le Global Interpreter Lock (GIL) et ses limitations : comprenez les contraintes du multithreading

Le Global Interpreter Lock (GIL) est un mécanisme de Python qui limite l'exécution de code Python à un seul thread à la fois, même sur une machine multi-coeur. Cela signifie que le multithreading en Python ne permet pas d'obtenir un véritable parallélisme pour les tâches liées au CPU (CPU-bound tasks), c'est-à-dire les tâches qui passent la plupart de leur temps à effectuer des calculs.

Le GIL a été introduit pour simplifier la gestion de la mémoire en Python (en particulier le ramasse-miettes), et pour faciliter l'intégration de bibliothèques C qui ne sont pas thread-safe. Cependant, il est devenu un goulot d'étranglement pour les performances dans certains cas.

Le GIL ne limite pas l'exécution concurrente des tâches liées aux entrées/sorties (I/O-bound tasks), c'est-à-dire les tâches qui passent la plupart de leur temps à attendre des données (par exemple, une réponse d'un serveur, une lecture de fichier, une entrée de l'utilisateur). Dans ce cas, le multithreading peut améliorer les performances, car un thread peut continuer à s'exécuter pendant qu'un autre thread attend.

Si vous avez besoin d'un véritable parallélisme pour des tâches liées au CPU, vous devez utiliser le multiprocessing (que nous verrons plus loin) ou des bibliothèques comme Numba ou Cython (qui permettent de contourner le GIL).

Nous verrons comment le GIL fonctionne, quelles sont ses limitations, et comment choisir entre le multithreading et le multiprocessing en fonction de votre problème.

Introduction au multiprocessing (module `multiprocessing`) : exécutez des tâches en parallèle

Le module `multiprocessing` vous permet de créer et de gérer des processus, au lieu de threads. Un processus est une instance d'un programme en cours d'exécution, qui possède sa propre mémoire et son propre interpréteur Python. Contrairement aux threads, les processus ne sont pas limités par le GIL, ce qui vous permet d'obtenir un véritable parallélisme, même pour les tâches liées au CPU.

Vous créez un processus en créant un objet `Process` (du module `multiprocessing`), en lui passant une fonction cible, et en appelant la méthode `start()` du processus. Le processus s'exécute alors en parallèle du processus principal (le processus qui a créé le processus).

Vous pouvez attendre qu'un processus se termine en appelant sa méthode `join()`. Vous pouvez également utiliser des queues (Queues), des pipes (Pipes) et d'autres primitives de communication inter-processus (du module `multiprocessing`) pour échanger des données entre les processus et coordonner leur exécution.

Le multiprocessing est plus lourd que le multithreading (il consomme plus de mémoire et prend plus de temps à démarrer), mais il peut offrir de meilleures performances pour les tâches liées au CPU.

Nous verrons comment utiliser le module `multiprocessing`, comment créer et démarrer des processus, comment attendre qu'ils se terminent, et comment utiliser des primitives de communication inter-processus.

Communication entre processus (Queues, Pipes) : échangez des données

Lorsque vous utilisez le multiprocessing, les processus ne partagent pas de mémoire par défaut. Si vous voulez qu'ils échangent des données, vous devez utiliser des mécanismes de communication inter-processus (IPC).

Le module `multiprocessing` fournit plusieurs mécanismes d'IPC, dont les queues (Queues) et les pipes (Pipes).

Une queue est une structure de données FIFO (First-In, First-Out) qui vous permet d'envoyer des objets d'un processus à un autre. Un processus peut mettre des objets dans la queue avec la méthode `put()`, et un autre processus peut les récupérer avec la méthode `get()`. La queue gère la synchronisation entre les processus.

Un pipe est une connexion bidirectionnelle entre deux processus. Il possède deux extrémités (deux objets `Connection`), une pour chaque processus. Un processus peut envoyer des données à l'autre processus en utilisant la méthode `send()` de son extrémité du pipe, et l'autre processus peut les recevoir en utilisant la méthode `recv()` de son extrémité du pipe.

Nous verrons comment utiliser les queues et les pipes, et comment les utiliser pour échanger des données entre les processus de manière sûre et efficace.

Programmation asynchrone avec `asyncio` : gérez de nombreuses tâches concurrentes

La programmation asynchrone est un style de programmation concurrente qui vous permet de gérer de nombreuses tâches concurrentes (principalement des tâches liées aux entrées/sorties) avec un seul thread. Elle est basée sur une boucle d'événements (event loop) et des coroutines.

En Python, vous pouvez faire de la programmation asynchrone avec le module `asyncio`.

Une coroutine est une fonction spéciale que vous définissez avec le mot-clé `async def`. Une coroutine peut être suspendue (mise en pause) et reprise (reprise) plusieurs fois pendant son exécution. Vous utilisez le mot-clé `await` pour suspendre l'exécution d'une coroutine jusqu'à ce qu'un résultat soit disponible (par exemple, une réponse d'un serveur, une lecture de fichier, etc.).

La boucle d'événements est le coeur d'une application `asyncio`. Elle gère l'exécution des coroutines, en les suspendant lorsqu'elles attendent un résultat, et en les reprenant lorsque le résultat est disponible. Elle gère également les entrées/sorties non bloquantes (non-blocking I/O), ce qui permet à votre programme de continuer à s'exécuter pendant qu'il attend des données.

`asyncio` est particulièrement adapté aux applications qui doivent gérer de nombreuses connexions réseau simultanées (comme les serveurs web, les clients de messagerie, les chatbots, etc.). Il peut offrir de meilleures performances que le multithreading dans ce type de situation, car il évite le coût de la création et de la gestion de nombreux threads.

Nous verrons comment utiliser `asyncio`, comment définir des coroutines, comment utiliser `await`, comment gérer la boucle d'événements, et comment effectuer des entrées/sorties non bloquantes.