lardon3d/docs/development/concurrency.md
2026-08-08 13:22:57 +02:00

184 lines
4.6 KiB
Markdown

# Règles de concurrence
## Vue d'ensemble
Lardon3D utilise un modèle de concurrence à thread unique pour ncurses
et un modèle multi-thread pour le traitement. La séparation est stricte :
le thread ncurses ne fait jamais de travail métier, et les workers ne
touchent jamais ncurses.
## Modèle de concurrence
```text
Thread principal (ncurses)
├── Gestion des entrées
├── Affichage TUI
└── Orchestration
Worker thread
├── Exécution des tâches
├── Calculs métier
└── Écritures de résultats
```
## Règles fondamentales
### 1. ncurses appartient au thread principal
```c
// ✅ Correct : appel depuis le thread principal
mvprintw(0, 0, "Progression: %d%%", progress);
// ❌ Interdit : appel depuis un worker
// mvprintw() dans un thread secondaire
```
### 2. Variables partagées protégées par mutex
```c
// ✅ Correct
pthread_mutex_lock(&queue->mutex);
queue->count++;
pthread_mutex_unlock(&queue->mutex);
// ❌ Interdit
// queue->count++; sans protection
```
### 3. Variables de condition pour la synchronisation
```c
// Producteur (scheduler)
pthread_mutex_lock(&queue->mutex);
queue->ready = true;
pthread_cond_signal(&queue->cond);
pthread_mutex_unlock(&queue->mutex);
// Consommateur (worker)
pthread_mutex_lock(&queue->mutex);
while (!queue->ready) {
pthread_cond_wait(&queue->cond, &queue->mutex);
}
// traitement
pthread_mutex_unlock(&queue->mutex);
```
### 4. Pas de callback ncurses depuis un worker
```c
// ✅ Correct : le worker signale au thread principal
void worker_callback(task_t *task, void *userdata) {
shared_state_t *state = userdata;
pthread_mutex_lock(&state->mutex);
state->result_ready = true;
pthread_cond_signal(&state->cond);
pthread_mutex_unlock(&state->mutex);
}
// ❌ Interdit : appel ncurses depuis le worker
// void worker_callback(...) {
// mvprintw(...);
// }
```
## Primitives utilisées
| Primitive | Usage |
|---|---|
| `pthread_mutex_t` | Protection des données partagées |
| `pthread_cond_t` | Synchronisation producteur/consommateur |
| `pthread_create()` | Création des workers |
| `pthread_join()` | Attente de fin des workers |
| `pthread_cancel()` | Annulation d'un worker (dernier recours) |
## Invariants de concurrence
1. **Un seul thread ncurses** : ncurses n'est jamais appelé depuis un
worker. Toute mise à jour de l'UI passe par des variables partagées
protégées.
2. **Mutex hiérarchique** : si plusieurs mutex sont acquis, toujours dans
le même ordre pour éviter les deadlocks.
3. **Annulation coopérative** : les workers vérifient périodiquement un
drapeau d'annulation. Pas d'interruption brutale sauf dernier recours.
4. **Réservation atomique** : la réservation du gouverneur est atomique.
Deux threads ne peuvent pas obtenir la même réservation.
5. **Pas de callback sans réservation** : aucun callback de tâche n'est
invoqué sans réservation active. Cet invariant est maintenu même en
présence d'erreurs.
## Anti-patterns
### Deadlock
```c
// ❌ Risque de deadlock
pthread_mutex_lock(&mutex_a);
pthread_mutex_lock(&mutex_b); // attend mutex_b
// Dans un autre thread :
pthread_mutex_lock(&mutex_b);
pthread_mutex_lock(&mutex_a); // attend mutex_a → DEADLOCK
```
**Solution** : toujours acquérir les mutex dans le même ordre.
### Race condition
```c
// ❌ Race condition
if (task->state == TASK_STATE_QUEUED) {
task->state = TASK_STATE_RUNNING;
}
// ✅ Correct
pthread_mutex_lock(&task->mutex);
if (task->state == TASK_STATE_QUEUED) {
task->state = TASK_STATE_RUNNING;
}
pthread_mutex_unlock(&task->mutex);
```
### Use-after-free
```c
// ❌ Use-after-free
task_destroy(task);
task_callback(task); // task est libéré
// ✅ Correct
task_callback(task);
task_destroy(task);
```
## Validation
Pour tout ticket touchant la concurrence, exécuter :
```sh
# Build TSan
CC=clang meson setup build-tsan --wipe -Db_sanitize=thread -Db_lundef=false
meson compile -C build-tsan -j8
meson test -C build-tsan --print-errorlogs
```
TSan détecte automatiquement :
- les race conditions
- les deadlocks potentiels
- les signaux perdus
- les verrous non libérés
## Checklist de concurrence
Avant de livrer un ticket touchant la concurrence :
- [ ] Toutes les variables partagées sont protégées par un mutex
- [ ] Les mutex sont toujours libérés (même en cas d'erreur)
- [ ] Les variables de condition sont vérifiées dans une boucle `while`
- [ ] Aucun appel ncurses depuis un worker
- [ ] L'annulation est coopérative (pas de `pthread_cancel` sauf dernier recours)
- [ ] TSan ne signale aucune erreur
- [ ] Le build ASan ne signale aucune fuite mémoire liée aux threads