9. Concurrence et exemples de préparation intra
9.1 Une case producteur/consommateur et les conditions
ExemplePthread.cpp illustre un tampon d’une seule case : buffer=0 signifie vide, un entier positif plein. Le producteur parcourt 1 à MAX=100000000. Il verrouille the_mutex, attend sur condp tant que le tampon est plein, écrit la valeur, signale condc, puis déverrouille. Le consommateur prend le même mutex, attend sur condc tant que vide, remet le tampon à 0, signale condp, puis libère le mutex. Il ne doit jamais lire une case vide, ni le producteur écraser une pleine. Le code ne fournit pas un affichage détaillé des valeurs consommées.
Le mutex protège le prédicat partagé. pthread_cond_wait relâche le mutex pendant l’attente puis le reprend avant de revenir : le partenaire peut ainsi modifier l’état. Le while vérifie à nouveau le prédicat, plutôt que supposer qu’un réveil garantit la condition. Le signal est une notification à un thread en attente, pas un stockage de l’élément ou une réserve de permissions; l’état du tampon est durable.
pthread_mutex_init(...,NULL) utilise les attributs par défaut; pthread_mutex_lock obtient le verrou ou bloque, et pthread_mutex_unlock le libère dans le protocole du propriétaire. pthread_mutex_destroy ne s’emploie que sur un verrou déverrouillé dont aucun travailleur ne se sert encore. pthread_cond_init et pthread_cond_destroy gèrent la durée de vie des conditions; pthread_cond_signal notifie un thread en attente. Le programme initialise mutex/deux conditions, crée les deux travailleurs, joint les deux, puis détruit les objets : leur durée de vie encadre celle des utilisateurs. Le lab 6 fait un bref rappel de ces fonctions; le détail du protocole vient du code et du corrigé écrit.
La source officielle contient pthreaad_mutex_t, un caractère O au lieu de 0/NULL dans l’initialisation de condc, et une fonction consommateur retournant void* sans retour explicite final. Ces fautes empêchent de considérer le fichier comme une solution déjà compilée. L’analyse est statique; aucune source corrigée ou compilation n’est revendiquée.
9.2 Plusieurs mutex : rendre un cycle impossible
Interblocage.c définit 10 mutex, 2 threads et N_PIGE=1. Chaque travailleur boucle sans fin : tirer des indices distincts, verrouiller dans cet ordre, attendre usleep(3), libérer puis recommencer. Les joins attendent donc des travailleurs dont aucune fin normale n’est prévue. Le tirage ne peut fournir plus d’indices distincts que N_MUTEX; respecter N_PIGE≤N_MUTEX.
Avec une seule pige, les acquisitions montrées ne gardent pas un premier verrou en attendant un deuxième : elles ne créent pas le cycle multi-verrous visé. Si plusieurs indices sont tirés dans des ordres différents, T1 peut garder M1 en attendant M2 et T2 garder M2 en attendant M1. Chacun bloque l’autre.
Le BubbleSort fourni, mais commenté avant les locks, permet d’imposer un ordre global croissant. Toute attente va alors d’un indice plus petit vers un plus grand : une chaîne ne peut revenir au premier et former ce cycle. Cela empêche cette cause d’interblocage, sans prouver l’absence de tous les autres défauts du programme.
usleep(3) signifie 3 microsecondes, pas 3 secondes. Les conversions directes entier/pointeur des identifiants posent une question de portabilité. Les messages « acquiert » sont imprimés avant l’appel bloquant : leur apparition ne prouve pas encore que le verrou est détenu. Le commentaire sur deux verrous ne remplace pas la déclaration de dix. Ces observations proviennent du code accessible; elles ne reconstruisent pas le module théorique Interblocage non publié.
9.3 Préparation intra : comprendre et montrer un entrelacement
Le support de 19 pages est une ressource de préparation avec corrections, pas une tentative d’examen accomplie. Le lab 6 en présente la fin rapidement; les solutions détaillées ci-dessous sont attribuées au PDF.
Préemption et threads. Dans le choix FCFS/SJF/RR, la réponse préemptive est RR : SJF y désigne sa forme non préemptive, SRT étant la variante distincte. Deux threads d’un processus ne partagent pas la même pile, mais partagent l’espace d’adressage. Une commutation entre eux n’exige pas le changement d’espace d’adressage d’une commutation entre processus. État d’exécution propre et mémoire partagée se distinguent.
Test-and-Set. Si le test de l’état libre et sa mise à occupé étaient séparés, deux threads pourraient voir le verrou libre avant l’écriture de l’autre. Une opération atomique lie les deux; elle sert de base à l’acquisition exclusive et aux verrous tournants. Expliquer cette causalité répond mieux que citer seulement « concurrence »; l’oral accepte l’idée exprimée dans ses propres mots.
Trois erreurs dans l’objet partagé. Les méthodes peuvent être appelées dans un ordre quelconque par un nombre indéterminé de threads; on suppose le code compilable. Il faut donner chaque défaut et un scénario précis, lignes et changements de contexte :
Fonction3prend MonMutex1 ligne 32, puisinput<0provoque un retour ligne 35 avant unlock ligne 38. Le verrou reste détenu; des appels futurs qui en ont besoin peuvent bloquer. Chaque chemin de sortie de la section critique doit libérer le verrou.- T1 entre dans
Fonction1, prend M1 ligne 8 et est suspendu avant M2 ligne 10. T2 entre dansFonction2, prend M2 ligne 20 et attend M1 ligne 22. T1 reprend et attend M2 : chacun possède ce dont l’autre a besoin. Un ordre commun d’acquisition empêche cette inversion. Fonction4lit A pour remplir dix cases sans mutex, alors que des écrivains modifient A. Les mutex des autres méthodes ne protègent pas magiquement cette lecture : employer une protection cohérente entre accès concurrents. L’initialisation au constructeur et la destruction au destructeur ne réparent pas les erreurs d’utilisation.
Le troisième diagnostic suit le corrigé, sans prétendre à une preuve exhaustive de tous les entrelacements de l’objet. Une phrase vague sur un « risque de deadlock » ne remplace pas le scénario demandé.
Charger puis analyser un fichier. Deux threads doivent respecter chargement→analyse. Initialiser avant création un booléen doneT1=false, un mutex lock et une condition cond. T1 verrouille, réalise/annonce le chargement, écrit doneT1=true, signale puis déverrouille. T2 verrouille, attend dans while (!doneT1) pthread_cond_wait(&cond,&lock), puis analyse et déverrouille. Le wait libère le mutex pour permettre à T1 d’avancer et le reprend avant le test.
Si T2 arrive d’abord, il peut attendre sans empêcher le chargement. Si T1 finit avant son attente, T2 lit directement le booléen vrai et ne dépend pas d’un signal déjà passé. C’est la raison d’un prédicat durable en plus de la notification. Main joint les deux, détruit les objets et peut annoncer la fin. Ce protocole établit l’ordre logique indépendamment du hasard de l’ordonnanceur; un simple yield ne le ferait pas.
Deux traces RR à refaire. Un seul CPU; arrivées communes dans l’ordre P1, P2, P3, P4; durées 5/4/6/3; coût de commutation nul :
Code (text) · sélectionner pour copier
q=3 : P1[0,3] P2[3,6] P3[6,9] P4[9,12] P1[12,14] P2[14,15] P3[15,18]
q=2 : P1[0,2] P2[2,4] P3[4,6] P4[6,8] P1[8,10] P2[10,12]
P3[12,14] P4[14,15] P1[15,16] P3[16,18]
À q=2, P4 n’utilise qu’une unité lors de son second tour; P1 également au dernier. La file ne conserve pas les tâches finies. Vérifier les totaux 5/4/6/3 et la fin 18 dans les deux cas. Les deux quantums et les deux solutions sont utiles pour éviter de traiter q comme un simple changement de graduation du même dessin.