Principe
Les threads permettent dâavoir plusieurs fils dâexĂ©cution au sein dâun processus qui sâexĂ©cutent en mĂȘme temps.
Les diffĂ©rents threads dâun processus partagent les mĂȘmes donnĂ©es.
Les dangers de partager les mĂȘmes donnĂ©es
Si plusieurs threads utilisent les mĂȘmes donnĂ©es, il faut alors mettre en place des protections (mĂ©canismes dâexĂ©cution mutuelle par exemple)
Pour plus dâinformations, voir Partage de donnĂ©es et la data-concurrence
IntĂ©rĂȘts des threads
Il y a de nombreux intĂ©rĂȘts :
- Performance (si on a plusieurs processeurs) : Nous pouvons dans ce cas partager les tĂąches sur les diffĂ©rents cĆurs du processeur.
- Organisation du programme
- Effectuer différentes opérations en simultanées
- etc.
Termes et vocabulaires
Concepts fondamentaux :
- Thread : Un fil dâexĂ©cution au sein dâun processus. Imaginez plusieurs tĂąches sâexĂ©cutant en parallĂšle dans un mĂȘme programme.
- Processus : Un programme en cours dâexĂ©cution. Un processus peut contenir plusieurs threads.
- Concurrence : La capacitĂ© dâexĂ©cuter plusieurs tĂąches en mĂȘme temps.
- ParallĂ©lisme : LâexĂ©cution simultanĂ©e de plusieurs tĂąches sur plusieurs cĆurs de processeur (comme mentionnĂ© dans Principe).
- Deadlock : Une situation oĂč deux threads ou plus sont bloquĂ©s indĂ©finiment, chacun attendant quâun autre libĂšre une ressource. (voir Deadlock)
- Race condition : Une situation oĂč le rĂ©sultat dâune opĂ©ration dĂ©pend de lâordre dâexĂ©cution des instructions de plusieurs threads. (voir Dataracing)
- Threads lĂ©gers : Des threads qui ont une empreinte mĂ©moire plus faible et peuvent ĂȘtre créés en grand nombre. (voir âGreenâ threads)
- MĂ©moire partagĂ©e : Une zone de mĂ©moire accessible par tous les threads dâun processus.
- Planificateur : Le composant du systĂšme dâexploitation qui dĂ©cide quel thread doit sâexĂ©cuter Ă un moment donnĂ©. (voir Threads systĂšmes)
Mise en Ćuvre (C)
En C, la bibliothĂšque pthread
(POSIX threads) peut ĂȘtre utilisĂ©e
Créer et démarrer un nouveau thread
#include <pthread.h>
int pthread_create(pthread_t *thread,
pthread_attr_t *attr,
void *(*start_routine)(void*),
void *arg)
Arguments de la fonction :
start_routine
:(function (void v) -> void*)
thread
: Lâidentifiant du thread créé est enregistrĂ© dans*thread
.attr
: permet de passer des options pour la création du thread (NULL pour les valeurs par défaut)
Attendre un thread
On peut attendre la fin dâexĂ©cution du thread en utilisant la fonction :
int pthread_join(pthread_t thread, void** retval)
OĂč thread
est lâidentifiant du thread Ă attendre, et retval
permet de récupérer sa valeur de retour.
Terminer un thread
Le thread se termine à la fin de sa fonction, ou bien en utilisant la fonction :
void pthread_exit(void* retval)
OĂč retval
est la valeur retournée par le thread.
Exemple
#include <stdio.h>
#include <pthread.h>
// Fonction exécutée par le nouveau thread
void* task(void* arg) {
int thread_id = *((int*)arg);
printf("Hello from thread %d!\n", thread_id);
pthread_exit(NULL);
}
int main() {
pthread_t thread1, thread2;
int thread_ids[2] = {1, 2};
// Créer le premier thread
pthread_create(&thread1, NULL, task, &thread_ids[0]);
// Créer le deuxiÚme thread
pthread_create(&thread2, NULL, task, &thread_ids[1]);
// Attendre la fin des threads (facultatif)
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("Main thread exiting.\n");
return 0;
}
Voir Attendre un thread pour la fonction pthread_join
Mise en Ćuvre (Java)
Ressource utile : www.w3schools.com
Pour utiliser des threads en Java, il faut :
- Déclarer une classe qui hérite de
Thread
. - Implémenter la méthode
public void run()
, avec le code à exécuter pour le thread.
Afin de démarrer le thread, il faut appeler la méthode start()
.
On peut attendre la fin du thread avec join()
Exemple :
class Task implements Runnable {
private int threadId;
public Task(int threadId) {
this.threadId = threadId;
}
@Override
public void run() {
System.out.println("Hello from thread " + threadId + "!");
}
}
public class ThreadExample {
public static void main(String[] args) {
Thread thread1 = new Thread(new Task(1));
Thread thread2 = new Thread(new Task(2));
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread exiting.");
}
}
Types de threads
Threads systĂšmes
Les threads systĂšmes sont directement gĂ©rĂ©s par le systĂšme dâexploitation. Chaque thread systĂšme correspond Ă une entitĂ© dâordonnancement distincte pour le noyau. Cela signifie que le systĂšme dâexploitation alloue des ressources (comme du temps CPU) Ă chaque thread de maniĂšre indĂ©pendante.
Exemple
Imaginez une voiture avec plusieurs passagers. Chaque passager reprĂ©sente un thread systĂšme. Le conducteur (le systĂšme dâexploitation) dĂ©cide Ă quel moment chaque passager (thread) peut regarder par la fenĂȘtre (utiliser le processeur). Le conducteur peut dĂ©cider de changer de passager Ă tout moment, en fonction de diffĂ©rents critĂšres (prioritĂ©, temps dâexĂ©cution restant, etc.).
Avantages
- Flexibilité : Grande flexibilitĂ© dans la gestion des threads, car le systĂšme dâexploitation offre un contrĂŽle fin sur lâordonnancement.
- Performance : Les threads systĂšmes peuvent tirer pleinement parti des capacitĂ©s multicĆur des processeurs modernes.
Inconvénients
- CoĂ»t : La crĂ©ation et la gestion de threads systĂšmes peuvent ĂȘtre coĂ»teuses en termes de ressources systĂšme.
- Complexité : La programmation avec des threads systĂšmes peut ĂȘtre plus complexe, car il faut gĂ©rer explicitement la synchronisation et les communications entre les threads.
âGreenâ threads
Les âgreenâ threads sont des threads gĂ©rĂ©s par un ordonnanceur utilisateur, câest-Ă -dire un logiciel qui sâexĂ©cute au sein de lâapplication. Cet ordonnanceur dĂ©cide lui-mĂȘme quand passer dâun thread Ă lâautre, sans impliquer directement le systĂšme dâexploitation.
Exemple
Imaginez une troupe de théùtre oĂč chaque acteur reprĂ©sente un âgreenâ thread. Le metteur en scĂšne (lâordonnanceur utilisateur) dĂ©cide de lâordre dans lequel les acteurs entrent en scĂšne et de la durĂ©e de leur intervention. Le systĂšme dâexploitation (la salle de théùtre) ne sâoccupe que de fournir la scĂšne et les lumiĂšres, sans se soucier de la coordination des acteurs.
Avantages
- LĂ©gĂšreté : Les âgreenâ threads sont gĂ©nĂ©ralement plus lĂ©gers Ă crĂ©er et Ă gĂ©rer que les threads systĂšmes, car ils ne nĂ©cessitent pas lâintervention du systĂšme dâexploitation Ă chaque commutation de contexte.
- Flexibilité : Lâordonnanceur utilisateur peut implĂ©menter des stratĂ©gies dâordonnancement spĂ©cifiques aux besoins de lâapplication.
Inconvénients
- Performance : Les âgreenâ threads peuvent ĂȘtre moins performants que les threads systĂšmes, car lâordonnanceur utilisateur peut ne pas ĂȘtre aussi optimisĂ© que celui du systĂšme dâexploitation.
- Blocage : Si un âgreenâ thread est bloquĂ© (par exemple, en attendant une entrĂ©e/sortie), tous les autres âgreenâ threads du mĂȘme processus peuvent ĂȘtre bloquĂ©s, parce que lâordonnanceur utilisateur ne peut pas passer Ă un thread systĂšme.
Partage de données et la data-concurrence
Partager les mĂȘmes donnĂ©es entre plusieurs threads peut ĂȘtre TRĂS dangereux.
Race condition
Une race condition survient lorsquâun programme multithread accĂšde Ă des donnĂ©es partagĂ©es de maniĂšre non atomique et que le rĂ©sultat final dĂ©pend de lâordre dâexĂ©cution imprĂ©visible de ces accĂšs. En dâautres termes, câest une situation oĂč deux ou plusieurs threads tentent de modifier une mĂȘme donnĂ©e en mĂȘme temps, et le rĂ©sultat final dĂ©pend de lâordre dans lequel ces modifications sont effectuĂ©es.
Imaginez un compteur partagĂ© par deux threads. Chaque thread incrĂ©mente ce compteur plusieurs fois. Si les deux threads lisent la valeur actuelle du compteur en mĂȘme temps, puis lâincrĂ©mentent, et enfin Ă©crivent la nouvelle valeur, il est possible que lâune des incrĂ©mentations soit perdue. Par exemple, si les deux threads lisent la valeur 0, lâincrĂ©mentent et Ă©crivent la valeur 1, le compteur final affichera 1 au lieu de 2, car lâune des incrĂ©mentations a Ă©tĂ© Ă©crasĂ©e.
Pourquoi câest problĂ©matique ?
- RĂ©sultats imprĂ©visibles : Le comportement du programme devient non dĂ©terministe, câest-Ă -dire quâil peut produire des rĂ©sultats diffĂ©rents Ă chaque exĂ©cution, mĂȘme avec les mĂȘmes entrĂ©es.
- DifficultĂ© de dĂ©bogage : Les race-conditions sont souvent difficiles Ă reproduire et Ă corriger, car elles dĂ©pendent de facteurs tels que le temps dâexĂ©cution et le planificateur du systĂšme.
- Bugs subtils : Les erreurs causĂ©es par les race-conditions peuvent ĂȘtre trĂšs subtiles et difficiles Ă dĂ©tecter, surtout dans des programmes complexes.
Exemple
Imaginons un programme qui va ouvrir deux threads, le premier va ajouter 5 Ă v
et le deuxiĂšme va ajouter 10 Ă v
On aura alors la suite dâexĂ©cution suivante :
Thread 1 | Thread 2 |
---|---|
- lire la valeur de v | - lire la valeur de v |
- Ajouter 5 | - Ajouter 10 |
- Ecrire le résultat | - Ecrire le résultat |
Lorsque plusieurs threads accĂšdent Ă une mĂȘme ressource partagĂ©e (ici, la variable v
) et la modifient, il peut se produire des conditions de course. Ces conditions de course peuvent entraĂźner des rĂ©sultats incorrects, des pertes de donnĂ©es, ou mĂȘme des plantages de lâapplication.
Deadlock
Un deadlock (impasse en français) est une situation dans laquelle deux ou plusieurs threads sont bloquĂ©s indĂ©finiment, chacun attendant quâun autre libĂšre une ressource quâil dĂ©tient. Câest une forme de blocage mutuel qui peut paralyser complĂštement une application multithread.
Exemple concret
Imaginez deux philosophes assis autour dâune table ronde, chacun avec une fourchette dans chaque main. Pour manger, un philosophe a besoin des deux fourchettes Ă cĂŽtĂ© de lui. Si chaque philosophe prend sa fourchette gauche, puis attend que sa fourchette droite se libĂšre, aucun dâeux ne pourra jamais manger, car ils attendent tous que lâautre libĂšre sa fourchette.
Les quatre conditions nĂ©cessaires pour quâun deadlock se produisent :
- Exclusion mutuelle : Une ressource ne peut ĂȘtre utilisĂ©e que par un seul thread Ă la fois.
- Attente circulaire : Un ensemble de threads attendent chacun une ressource dĂ©tenue par le thread suivant de lâensemble.
- Non-privation : Un thread ne peut pas se faire retirer une ressource quâil dĂ©tient.
- Attente : Un thread doit attendre une ressource détenue par un autre thread.
ConsĂ©quences dâun deadlock:
- Blocage de lâapplication : Lâapplication peut devenir complĂštement inutilisable si les threads bloquĂ©s sont essentiels Ă son fonctionnement.
- Perte de ressources : Les ressources dĂ©tenues par les threads bloquĂ©s sont inutilisables jusquâĂ ce que le deadlock soit rĂ©solu.
- DifficultĂ© de dĂ©bogage : Les deadlocks peuvent ĂȘtre difficiles Ă dĂ©tecter et Ă reproduire, car ils dĂ©pendent souvent de lâordre dâexĂ©cution des threads.
Mécanismes de protection face aux deadlocks et aux race-conditions
Je dois m'en occuper, voici quelques ressources en attendant