first comit
This commit is contained in:
212
README.md
Normal file
212
README.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# 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.
|
||||
|
||||
```mermaid
|
||||
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.
|
||||
|
||||
<!-- end list -->
|
||||
|
||||
```mermaid
|
||||
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.
|
||||
|
||||
<!-- end list -->
|
||||
|
||||
```mermaid
|
||||
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.
|
||||
|
||||
<!-- end list -->
|
||||
|
||||
```mermaid
|
||||
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
|
||||
Reference in New Issue
Block a user