Objectif du chapitre
Dans un programme multithread, il peut être nécessaire de demander à un thread de terminer avant qu’il ait fini son travail.
POSIX fournit pour cela un mécanisme d’annulation coopérative basé principalement sur :
Code: Select all
pthread_cancel()
pthread_setcancelstate()
pthread_setcanceltype()
pthread_testcancel()
pthread_cleanup_push()
pthread_cleanup_pop()
Code: Select all
pthread_cancel()Il envoie une demande d’annulation.
Le moment où le thread se termine dépend ensuite :
- de son état d’annulation ;
- de son type d’annulation ;
- de l’existence de points d’annulation ;
- de la bonne libération de ses ressources.
1. Envoyer une demande d’annulation
La fonction utilisée pour demander l’annulation d’un thread est :
Code: Select all
#include <pthread.h>
int pthread_cancel(pthread_t thread);
Code: Select all
threadValeur de retour :
- en cas de succès ;
Code: Select all
0 - un numéro d’erreur positif en cas d’échec.
Code: Select all
pthread_t thread;
pthread_create(&thread, NULL, fonction_thread, NULL);
pthread_cancel(thread);
pthread_join(thread, NULL);
Code: Select all
pthread_cancel()Cela ne garantit pas encore que le thread est déjà terminé.
Le thread peut :
- être annulé immédiatement ;
- attendre d’atteindre un point d’annulation ;
- conserver la demande en attente ;
- ne jamais être annulé si l’annulation reste désactivée.
2. État d’annulation d’un thread
Chaque thread possède un état d’annulation.
Cet état détermine si une demande d’annulation peut être prise en compte.
La fonction correspondante est :
Code: Select all
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
Code: Select all
statePTHREAD_CANCEL_ENABLE
L’annulation est activée.
Une demande d’annulation peut être traitée selon le type d’annulation du thread.
C’est l’état par défaut d’un nouveau thread.
PTHREAD_CANCEL_DISABLE
L’annulation est désactivée.
Une demande envoyée avec
Code: Select all
pthread_cancel()Elle pourra être traitée plus tard lorsque le thread réactivera l’annulation.
Le second paramètre,
Code: Select all
oldstateExemple :
Code: Select all
int ancien_etat;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &ancien_etat);
/* Section critique non annulable */
pthread_setcancelstate(ancien_etat, NULL);
3. Type d’annulation
Lorsque l’annulation est activée, le type d’annulation détermine quand la demande sera exécutée.
La fonction utilisée est :
Code: Select all
#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);
PTHREAD_CANCEL_DEFERRED
L’annulation est différée.
La demande reste en attente jusqu’à ce que le thread atteigne un point d’annulation.
C’est le type par défaut.
C’est également le type recommandé dans la majorité des programmes.
PTHREAD_CANCEL_ASYNCHRONOUS
L’annulation est asynchrone.
Le thread peut être annulé presque à n’importe quel moment.
Ce mode est très dangereux, car le thread peut être interrompu :
- pendant qu’il détient un mutex ;
- pendant une allocation mémoire ;
- pendant la modification d’une structure ;
- entre deux instructions dépendantes ;
- alors qu’un invariant interne est temporairement invalide.
Code: Select all
int ancien_type;
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &ancien_type);
/* Travail annulable uniquement aux points prévus */
pthread_setcanceltype(ancien_type, NULL);
Code: Select all
PTHREAD_CANCEL_DEFERRED4. Les points d’annulation
Avec une annulation différée, le thread ne se termine pas immédiatement.
Il doit atteindre un point d’annulation.
Un point d’annulation est un emplacement où la bibliothèque vérifie si une demande d’annulation est en attente.
De nombreuses fonctions POSIX pouvant bloquer sont des points d’annulation.
Exemples courants :
Code: Select all
accept()
close()
connect()
fcntl()
open()
pause()
poll()
pread()
pselect()
pthread_cond_wait()
pthread_cond_timedwait()
read()
readv()
recv()
recvfrom()
recvmsg()
select()
send()
sendmsg()
sleep()
system()
tcdrain()
usleep()
wait()
waitpid()
write()
writev()
Certaines fonctions peuvent être définies comme points d’annulation par une implémentation sans être obligatoirement présentes dans toutes les listes.
Les fonctions bloquantes sont particulièrement importantes, car elles empêchent qu’un thread reste bloqué indéfiniment alors qu’une demande d’annulation a été envoyée.
Comportement général
Lorsqu’un thread avec annulation activée et différée atteint un point d’annulation :
- la demande en attente est détectée ;
- les gestionnaires de nettoyage sont exécutés ;
- les données spécifiques au thread sont nettoyées ;
- le thread se termine ;
- peut récupérer
Code: Select all
pthread_join().Code: Select all
PTHREAD_CANCELED
5. Créer explicitement un point d’annulation
Un thread peut exécuter une longue boucle sans appeler aucune fonction constituant un point d’annulation.
Dans ce cas, une demande différée pourrait ne jamais être traitée.
POSIX fournit :
Code: Select all
#include <pthread.h>
void pthread_testcancel(void);
Elle teste si une demande d’annulation est en attente.
Si aucune demande n’existe, elle retourne normalement.
Si une demande existe et que l’annulation est activée, le thread est annulé.
Exemple :
Code: Select all
void *fonction_thread(void *arg)
{
for (;;) {
effectuer_un_bloc_de_calcul();
pthread_testcancel();
}
return NULL;
}
- les boucles de calcul ;
- les traitements CPU longs ;
- les algorithmes qui n’appellent pas de fonction bloquante ;
- les threads qui doivent rester réactifs à une demande d’arrêt.
6. Récupérer le résultat d’un thread annulé
La fonction
Code: Select all
pthread_join()Lorsqu’un thread est annulé, la valeur récupérée vaut :
Code: Select all
PTHREAD_CANCELED
Code: Select all
void *resultat;
pthread_cancel(thread);
pthread_join(thread, &resultat);
if (resultat == PTHREAD_CANCELED) {
printf("Le thread a été annulé\n");
} else {
printf("Le thread s'est terminé normalement\n");
}
Code: Select all
pthread_join()Cela permet de récupérer ses ressources système.
7. Exemple simple d’annulation différée
Code: Select all
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void *fonction_thread(void *arg)
{
(void)arg;
puts("thread : démarré");
for (;;) {
puts("thread : boucle");
sleep(1);
}
return NULL;
}
int main(void)
{
pthread_t thread;
void *resultat;
int erreur;
erreur = pthread_create(&thread, NULL, fonction_thread, NULL);
if (erreur != 0) {
fprintf(stderr, "pthread_create : %d\n", erreur);
return EXIT_FAILURE;
}
sleep(3);
erreur = pthread_cancel(thread);
if (erreur != 0) {
fprintf(stderr, "pthread_cancel : %d\n", erreur);
return EXIT_FAILURE;
}
erreur = pthread_join(thread, &resultat);
if (erreur != 0) {
fprintf(stderr, "pthread_join : %d\n", erreur);
return EXIT_FAILURE;
}
if (resultat == PTHREAD_CANCELED) {
puts("main : thread annulé");
} else {
puts("main : fin normale");
}
return EXIT_SUCCESS;
}
Code: Select all
sleep()La demande envoyée par
Code: Select all
pthread_cancel()8. Pourquoi l’annulation peut provoquer des fuites ou des deadlocks
Supposons qu’un thread effectue les opérations suivantes :
Code: Select all
pthread_mutex_lock(&mutex);
buffer = malloc(4096);
/* point d’annulation */
traiter(buffer);
free(buffer);
pthread_mutex_unlock(&mutex);
- le bloc alloué peut ne jamais être libéré ;
- le mutex peut rester verrouillé ;
- les autres threads peuvent rester bloqués ;
- l’état partagé peut rester incohérent.
9. Gestionnaires de nettoyage
POSIX fournit deux macros :
Code: Select all
pthread_cleanup_push()
pthread_cleanup_pop()
Code: Select all
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
pthread_cleanup_push()
Ajoute un gestionnaire de nettoyage au sommet de la pile du thread.
La fonction de nettoyage doit avoir cette forme :
Code: Select all
void nettoyage(void *arg)
{
/* Libération d’une ressource */
}
pthread_cleanup_pop()
Retire le gestionnaire placé au sommet de la pile.
Si
Code: Select all
executeSi
Code: Select all
executeOrdre d’exécution
Les gestionnaires sont exécutés dans l’ordre inverse de leur enregistrement.
Il s’agit d’une pile LIFO :
Code: Select all
pthread_cleanup_push(nettoyage_1, arg1);
pthread_cleanup_push(nettoyage_2, arg2);
Code: Select all
nettoyage_2
nettoyage_1
10. Quand les gestionnaires sont-ils exécutés ?
Les gestionnaires de nettoyage sont exécutés automatiquement dans plusieurs situations importantes :
- lorsqu’un thread est annulé ;
- lorsque le thread appelle ;
Code: Select all
pthread_exit() - lorsque est utilisé.
Code: Select all
pthread_cleanup_pop(1)
Code: Select all
returnCode: Select all
pthread_cleanup_pop(1)Il faut donc organiser le code pour que chaque appel à
Code: Select all
pthread_cleanup_push()Code: Select all
pthread_cleanup_pop()11. Attention : ce sont souvent des macros
Sur de nombreuses implémentations,
Code: Select all
pthread_cleanup_push()Code: Select all
pthread_cleanup_pop()Elles doivent donc être utilisées dans la même portée.
Exemple correct :
Code: Select all
pthread_cleanup_push(nettoyage, ressource);
/* Travail */
pthread_cleanup_pop(1);
Code: Select all
if (condition)
pthread_cleanup_push(nettoyage, ressource);
/* Code */
pthread_cleanup_pop(1);
Une forme plus sûre est :
Code: Select all
if (condition) {
pthread_cleanup_push(nettoyage, ressource);
/* Code */
pthread_cleanup_pop(1);
}
12. Nettoyer une allocation mémoire
Code: Select all
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void liberer_buffer(void *arg)
{
free(arg);
puts("buffer libéré");
}
static void *fonction_thread(void *arg)
{
(void)arg;
void *buffer = malloc(4096);
if (buffer == NULL) {
return NULL;
}
pthread_cleanup_push(liberer_buffer, buffer);
for (;;) {
sleep(1);
}
pthread_cleanup_pop(1);
return NULL;
}
Code: Select all
sleep()Code: Select all
free()Le buffer ne fuit donc pas.
13. Nettoyer un mutex
Code: Select all
#include <pthread.h>
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static void deverrouiller_mutex(void *arg)
{
pthread_mutex_t *m = arg;
pthread_mutex_unlock(m);
}
static void *fonction_thread(void *arg)
{
(void)arg;
pthread_mutex_lock(&mutex);
pthread_cleanup_push(deverrouiller_mutex, &mutex);
/* Travail potentiellement annulable */
pthread_cleanup_pop(1);
return NULL;
}
Cela évite un blocage permanent des autres threads.
14. Cas particulier de pthread_cond_wait()
La fonction :
Code: Select all
pthread_cond_wait()
Lorsqu’un thread est annulé pendant l’attente sur une variable de condition, POSIX garantit que le mutex associé est repris avant l’exécution des gestionnaires de nettoyage.
Cela permet à un gestionnaire de nettoyage d’appeler correctement :
Code: Select all
pthread_mutex_unlock()
Code: Select all
pthread_mutex_lock(&mutex);
pthread_cleanup_push(deverrouiller_mutex, &mutex);
while (!condition) {
pthread_cond_wait(&cond, &mutex);
}
pthread_cleanup_pop(1);
Sans reprise préalable du mutex, le gestionnaire pourrait tenter de déverrouiller un mutex que le thread ne détient pas.
15. Exemple complet avec buffer, mutex et variable de condition
Code: Select all
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static int disponible = 0;
static void nettoyer_mutex(void *arg)
{
pthread_mutex_t *m = arg;
pthread_mutex_unlock(m);
puts("nettoyage : mutex déverrouillé");
}
static void nettoyer_buffer(void *arg)
{
free(arg);
puts("nettoyage : buffer libéré");
}
static void *fonction_thread(void *arg)
{
(void)arg;
void *buffer = malloc(4096);
if (buffer == NULL) {
return NULL;
}
pthread_cleanup_push(nettoyer_buffer, buffer);
pthread_mutex_lock(&mutex);
pthread_cleanup_push(nettoyer_mutex, &mutex);
while (!disponible) {
pthread_cond_wait(&cond, &mutex);
}
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
return NULL;
}
int main(void)
{
pthread_t thread;
void *resultat;
if (pthread_create(&thread, NULL, fonction_thread, NULL) != 0) {
return EXIT_FAILURE;
}
sleep(1);
pthread_cancel(thread);
pthread_join(thread, &resultat);
if (resultat == PTHREAD_CANCELED) {
puts("thread annulé proprement");
}
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return EXIT_SUCCESS;
}
Code: Select all
1. le mutex est repris par pthread_cond_wait()
2. nettoyer_mutex() est exécuté
3. nettoyer_buffer() est exécuté
4. le thread se termine
16. Désactiver l’annulation dans une section critique
Un gestionnaire de nettoyage n’est pas toujours suffisant.
Certaines opérations doivent être terminées entièrement avant qu’une annulation puisse être acceptée.
Exemple :
Code: Select all
int ancien_etat;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &ancien_etat);
mettre_a_jour_structure();
ecrire_etat_sur_disque();
valider_transaction();
pthread_setcancelstate(ancien_etat, NULL);
Lorsque l’ancien état est restauré, la demande peut être traitée au prochain point d’annulation.
17. Annulation asynchrone
Le mode asynchrone est activé avec :
Code: Select all
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
Cela rend très difficile la garantie d’un état cohérent.
Exemple dangereux :
Code: Select all
pthread_mutex_lock(&mutex);
compteur++;
liste = modifier_liste(liste);
pthread_mutex_unlock(&mutex);
Code: Select all
pthread_mutex_lock()Code: Select all
pthread_mutex_unlock()Il peut aussi être annulé pendant une fonction de la bibliothèque comme
Code: Select all
malloc()La bibliothèque elle-même peut alors rester dans un état interne indéterminé.
Règle pratique
N’utiliser l’annulation asynchrone que pour du code extrêmement limité qui :
- n’alloue pas de mémoire ;
- ne détient aucun verrou ;
- ne modifie aucune structure partagée ;
- n’appelle presque aucune fonction de bibliothèque ;
- ne dépend d’aucun invariant complexe.
18. Héritage des paramètres d’annulation
Un nouveau thread démarre généralement avec :
Code: Select all
PTHREAD_CANCEL_ENABLE
PTHREAD_CANCEL_DEFERRED
Chaque thread doit configurer explicitement son propre comportement lorsqu’un autre choix est nécessaire.
19. Annulation et fork()
Après un appel à
Code: Select all
fork()L’état d’annulation du thread appelant est conservé dans l’enfant.
Le programme doit néanmoins rester très prudent, car les verrous détenus par d’autres threads au moment du
Code: Select all
fork()20. Erreurs de conception courantes
1. Croire que pthread_cancel() tue immédiatement le thread
Il s’agit uniquement d’une demande.
Avec le mode différé, le thread doit atteindre un point d’annulation.
2. Oublier pthread_join()
Un thread annulé doit toujours être rejoint s’il est joignable.
3. Annuler un thread qui détient un mutex
Sans gestionnaire de nettoyage, le mutex peut rester verrouillé.
4. Ne pas protéger les allocations
Un thread annulé peut abandonner des blocs mémoire ou des descripteurs ouverts.
5. Utiliser PTHREAD_CANCEL_ASYNCHRONOUS sans nécessité
Cela rend le code presque impossible à raisonner correctement.
6. Ne jamais appeler de point d’annulation
Une boucle CPU sans appel bloquant doit invoquer
Code: Select all
pthread_testcancel()7. Mal apparier cleanup_push et cleanup_pop
Les deux macros doivent rester dans le même bloc lexical.
8. Utiliser return en pensant déclencher les gestionnaires
Un retour normal de la fonction du thread ne garantit pas l’exécution automatique des gestionnaires encore empilés.
21. Alternative recommandée : arrêt coopératif explicite
Dans de nombreux programmes, un drapeau d’arrêt explicite est plus simple et plus sûr que
Code: Select all
pthread_cancel()Exemple avec variable atomique :
Code: Select all
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <unistd.h>
static atomic_bool arret = false;
static void *fonction_thread(void *arg)
{
(void)arg;
while (!atomic_load(&arret)) {
effectuer_travail();
}
return NULL;
}
int main(void)
{
pthread_t thread;
pthread_create(&thread, NULL, fonction_thread, NULL);
sleep(3);
atomic_store(&arret, true);
pthread_join(thread, NULL);
return 0;
}
- flux de contrôle plus explicite ;
- nettoyage plus facile à comprendre ;
- pas d’interruption cachée dans une fonction ;
- meilleure portabilité logique ;
- moins de risques de deadlock.
Code: Select all
pthread_cancel()22. Modèle professionnel recommandé
Un modèle robuste consiste à :
- utiliser l’annulation différée ;
- installer les gestionnaires de nettoyage avant d’acquérir des ressources annulables ;
- limiter la durée des sections où l’annulation est désactivée ;
- insérer dans les longues boucles ;
Code: Select all
pthread_testcancel() - rejoindre systématiquement les threads ;
- privilégier un arrêt coopératif lorsque cela suffit.
Code: Select all
static void *fonction_thread(void *arg)
{
int ancien_etat;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &ancien_etat);
initialiser_ressources();
pthread_cleanup_push(nettoyer_ressources, arg);
pthread_setcancelstate(ancien_etat, NULL);
for (;;) {
effectuer_travail();
pthread_testcancel();
}
pthread_cleanup_pop(1);
return NULL;
}
23. API récapitulatives
pthread_cancel()
Code: Select all
int pthread_cancel(pthread_t thread);
pthread_setcancelstate()
Code: Select all
int pthread_setcancelstate(int state, int *oldstate);
pthread_setcanceltype()
Code: Select all
int pthread_setcanceltype(int type, int *oldtype);
pthread_testcancel()
Code: Select all
void pthread_testcancel(void);
pthread_cleanup_push()
Code: Select all
void pthread_cleanup_push(void (*routine)(void *), void *arg);
pthread_cleanup_pop()
Code: Select all
void pthread_cleanup_pop(int execute);
pthread_join()
Code: Select all
int pthread_join(pthread_t thread, void **retval);
Code: Select all
PTHREAD_CANCELED24. Valeurs et constantes importantes
Code: Select all
PTHREAD_CANCEL_ENABLE
PTHREAD_CANCEL_DISABLE
PTHREAD_CANCEL_DEFERRED
PTHREAD_CANCEL_ASYNCHRONOUS
PTHREAD_CANCELED
- : annulation activée ;
Code: Select all
PTHREAD_CANCEL_ENABLE - : annulation désactivée ;
Code: Select all
PTHREAD_CANCEL_DISABLE - : annulation aux points d’annulation ;
Code: Select all
PTHREAD_CANCEL_DEFERRED - : annulation possible presque partout ;
Code: Select all
PTHREAD_CANCEL_ASYNCHRONOUS - : valeur récupérée par
Code: Select all
PTHREAD_CANCELEDaprès annulation.Code: Select all
pthread_join()
25. Résumé final
L’annulation POSIX est un mécanisme de terminaison coopérative.
Code: Select all
pthread_cancel()
Le mode normal est :
Code: Select all
PTHREAD_CANCEL_ENABLE
PTHREAD_CANCEL_DEFERRED
Code: Select all
pthread_testcancel()
Les gestionnaires :
Code: Select all
pthread_cleanup_push()
pthread_cleanup_pop()
L’annulation asynchrone est extrêmement risquée et doit presque toujours être évitée.
Dans un programme moderne et robuste, il faut préférer :
- l’annulation différée ;
- le nettoyage structuré ;
- l’arrêt coopératif explicite ;
- l’attente finale avec .
Code: Select all
pthread_join()
