2025-12-15 15:50:40 +01:00
2025-12-15 15:50:40 +01:00
2025-12-15 15:50:40 +01:00
2025-12-15 15:50:40 +01:00
2025-12-15 15:50:40 +01:00
2025-12-15 15:50:40 +01:00
2025-12-15 15:50:40 +01:00

Démonstration : introduction à la programmation concurrente en Python

Objectifs Pédagogiques

À la fin de cette démonstration, vous serez capable de :

  1. Identifier la différence fondamentale entre un problème I/O-bound (limité par les attentes) et un problème CPU-bound (limité par la puissance de calcul).
  2. Mettre en œuvre une solution de concurrence avec asyncio pour accélérer les tâches I/O-bound.
  3. Mettre en œuvre une solution de parallélisme avec multiprocessing pour accélérer les tâches CPU-bound.
  4. Comprendre quand utiliser l'un ou l'autre de ces modèles.
  5. Initiation au débogage via l'IDE PyCharm.

Partie 1 : le problème I/O-Bound (concurrence avec asyncio)

Scénario : nous avons une série de tâches qui passent la majorité de leur temps à attendre une ressource externe (par exemple, une réponse d'API, une requête de base de données, ou une lecture sur un disque lent). Nous allons simuler cette attente avec une pause.

Exercice 1.1 : l'approche séquentielle (le point de départ)

  1. Ouvrez le fichier 1_sync_simulation.py.

  2. Lisez attentivement le code, en particulier la fonction worker_task. Notez l'utilisation de time.sleep(), une fonction bloquante qui met en pause tout le programme.

    sequenceDiagram
        participant main as "Programme Principal"
        participant task1 as "Tâche 1"
        participant task2 as "Tâche 2"
        participant task3 as "Tâche 3 (etc...)"
    
        main->>task1: Démarrer
        note right of task1: "time.sleep(1s) (BLOQUANT)"
        task1-->>main: Fini
    
        main->>task2: Démarrer
        note right of task2: "time.sleep(1s) (BLOQUANT)"
        task2-->>main: Fini
    
        main->>task3: Démarrer
        note right of task3: "time.sleep(1s) (BLOQUANT)"
        task3-->>main: Fini
    
  3. Exécutez le script via le bouton Run de PyCharm.

  4. Observez le temps d'exécution total. Il devrait être approximativement égal à NUM_TASKS * SLEEP_DURATION. Prenez note de cette valeur.

Exercice 1.2 : l'approche Asynchrone (à compléter)

Maintenant, nous allons modifier le script pour qu'il exécute ces mêmes tâches d'attente de manière concurrente.

  1. Ouvrez le fichier 2_async_simulation.py. Ce fichier est incomplet et contient plusieurs TODO.

  2. Suivez les instructions ci-dessous pour le compléter :

    • TODO 1 dans la fonction worker_task_async : une fonction qui utilise await doit être déclarée comme une "coroutine".

      Modifiez la signature de la fonction worker_task_async pour en faire une coroutine native.

    • TODO 2 dans la fonction worker_task_async : nous devons remplacer l'attente bloquante par son équivalent non-bloquant du module asyncio.

      Trouvez la fonction adéquate dans le module asyncio pour simuler une attente et utilisez await pour l'appeler.

    • TODO 3 dans la fonction main : la puissance de asyncio vient de sa capacité à lancer de nombreuses tâches et à les gérer simultanément.

      Utilisez la fonction asyncio.gather() pour lancer toutes les coroutines de la liste tasks en concurrence. N'oubliez pas d'utiliser await pour attendre que gather() se termine.

    sequenceDiagram
        participant main as "Programme Principal"
        participant eventLoop as "Event Loop (asyncio)"
        participant task1 as "Tâche 1"
        participant task2 as "Tâche 2"
        participant task3 as "Tâche 3"
    
        activate eventLoop
        main->>eventLoop: "asyncio.run(main)"
    
        eventLoop->>task1: Lancer (via gather)
        eventLoop->>task2: Lancer (via gather)
        eventLoop->>task3: Lancer (via gather)
    
        note over task1,task3: Les tâches démarrent quasi-simultanément
    
        task1->>eventLoop: "await asyncio.sleep(1s)" (NON-BLOQUANT)
        note right of task1: Rend le contrôle
    
        task2->>eventLoop: "await asyncio.sleep(1s)" (NON-BLOQUANT)
        note right of task2: Rend le contrôle
    
        task3->>eventLoop: "await asyncio.sleep(1s)" (NON-BLOQUANT)
        note right of task3: Rend le contrôle
    
        eventLoop-->>task1: Reprendre (après 1s)
        task1-->>eventLoop: Fini
    
        eventLoop-->>task2: Reprendre (après 1s)
        task2-->>eventLoop: Fini
    
        eventLoop-->>task3: Reprendre (après 1s)
        task3-->>eventLoop: Fini
    
        eventLoop-->>main: Terminé
        deactivate eventLoop
    
  3. Exécutez le script via le bouton Run de PyCharm.

  4. Comparez le temps d'exécution avec la version synchrone. Que remarquez-vous ? Pourquoi une telle différence ?


Partie 2 : le problème CPU-Bound (parallélisme avec multiprocessing)

Scénario : nous changeons de problème. Nous avons maintenant des tâches qui ne consistent pas à attendre, mais à effectuer des calculs mathématiques très intensifs. Chaque tâche utilise 100% d'un cœur de processeur.

Exercice 2.1 : l'approche séquentielle et parallèle (à compléter)

  1. Ouvrez le fichier 3_multiprocessing_demo.py.

    • La fonction heavy_analysis simule notre tâche de calcul lourd.
    • La fonction run_sequential est déjà complète. Elle exécute les tâches sur un seul cœur, l'une après l'autre.
    graph TD
        subgraph "Exécution Séquentielle"
            subgraph "CPU Core 1"
                A[Tâche 1: Calcul lourd] --> B[Tâche 2: Calcul lourd] --> C[Tâche 3: Calcul lourd] --> D[Fin]
            end
    
            subgraph "Autres Cores"
                idle1[Inutilisé]
                idle2[Inutilisé]
                idle3[Inutilisé]
            end
        end
    
    • La fonction run_parallel est incomplète. C'est elle que nous allons modifier.
  2. Suivez les instructions pour compléter la fonction run_parallel :

    • TODO 1 : pour paralléliser le travail sur plusieurs cœurs, nous avons besoin d'un "pool de processus". La bibliothèque concurrent.futures fournit un gestionnaire de contexte parfait pour cela.

      Instanciez un ProcessPoolExecutor en utilisant un bloc with. Vous pouvez laisser le nombre de workers par défaut pour qu'il utilise tous vos cœurs CPU.

    • TODO 2 : une fois le pool de processus créé, il faut lui donner la liste des tâches à exécuter.

      Utilisez la méthode .map() de l'exécuteur pour appliquer la worker_function à chaque élément de la liste tasks. Cette méthode distribue le travail et retourne les résultats.

    graph TD
        subgraph "Exécution Parallèle"
            pool("ProcessPoolExecutor")
    
            subgraph "CPU Core 1"
                T1[Tâche 1: Calcul]
            end
    
            subgraph "CPU Core 2"
                T2[Tâche 2: Calcul]
            end
    
            subgraph "CPU Core 3"
                T3[Tâche 3: Calcul]
            end
    
            subgraph "CPU Core N..."
                TN[Tâche N: Calcul]
            end
    
            pool --> T1
            pool --> T2
            pool --> T3
            pool --> TN
    
            T1 --> R[Collecte des résultats]
            T2 --> R
            T3 --> R
            TN --> R
        end
    
  3. Exécutez le script via le bouton Run de PyCharm.

  4. Le script va d'abord lancer la version séquentielle, puis la version parallèle, et enfin afficher un résumé. Analysez le facteur d'accélération (Speedup). Est-il proche du nombre de cœurs de votre processeur ?


Conclusion et synthèse

Discutez des résultats que vous avez obtenus. Vous devriez maintenant être capable de répondre à la question fondamentale :

"Mon programme est lent. Dois-je utiliser asyncio ou multiprocessing ?"

Type de Problème Goulot d'étranglement Solution Mots-clés
I/O-Bound Attente (Réseau, Disque, BDD) Asynchronisme (asyncio) async, await, gather
CPU-Bound Calculs intensifs (CPU à 100%) Parallélisme (multiprocessing) ProcessPoolExecutor, .map

Dans certains cas, une combinaison des deux approches peut être nécessaire, mais cela sort du cadre de cette introduction.

Vous avez maintenant exploré les deux piliers de la programmation performante en Python. N'hésitez pas à modifier les variables comme NUM_TASKS dans les scripts pour voir comment les temps d'exécution évoluent.


Bonus : débogage avec PyCharm

Sur l'ensemble de la suite d'IDE JetBrains (WebStorm pour Javascript, PyCharm pour Python, etc.), le débogage est un outil puissant pour comprendre le flux d'exécution et inspecter les variables en temps réel.

  1. Mettez un point d'arrêt (breakpoint) par exemple à la ligne 42 du fichier 1_sync_simulation.py. Pour cela, cliquez dans la marge gauche à côté du numéro de ligne.
  2. Lancez le mode débogage via le bouton Debug de PyCharm.
  3. Utilisez les commandes de débogage pour exécuter le code pas à pas (Step Into My Code, etc.) et observer les valeurs des variables en temps réel
Description
No description provided
Readme 73 KiB
Languages
Python 100%