SE2 : Introduction au multithreading
SE2: Système d'exploitation
Printemps 2024
Instructrices: GUEALIA Nadia


Illustration courtesy of Ecy King, CS110 Champion, Spring 2021
Comment pouvons-nous avoir de la concurrence au sein d’un même processus ?
Objectifs
- Découvrez comment les threads permettent la concurrence au sein d'un même processus
- Comprenez les différences entre les threads et les processus
- Découvrez comment le partage du même espace d'adressage virtuel entre différent threads peut être dangereux
From Processes to Threads
- Le multitraitement (multiprocessing) nous a permis de générer d'autres processus pour effectuer des tâches ou exécuter des programmes
- Puissant ; peut exécuter/attendre d'autres programmes, sécurisé (espace mémoire séparé), communiquer avec des tuyaux (pipes) et des signaux
- Mais limité ; la communication interprocessus est lourde, il est difficile de partager des données/de coordonner
- Existe-t-il un autre moyen d’obtenir une concurrence au-delà du multitraitement qui gère ces compromis différemment ?
Nous pouvons avoir une concurrence au sein d'un même processus en utilisant des threads : des séquences d'exécution indépendantes au sein d'un même processus.
- Les threads nous permettent d'exécuter simultanément plusieurs fonctions de notre programme
- Le multithreading est très courant pour paralléliser les tâches, en particulier sur plusieurs cores
- En C: générez un thread en utilisant pthread_create et spécifiez la fonction que vous souhaitez que le thread exécute (en passant éventuellement des paramètres !)
- Le gestionnaire de threads bascule entre les threads en cours d'exécution comme le scheduler du système d'exploitation bascule entre les processus en cours d'exécution
- Chaque thread fonctionne dans le même processus, ils partagent donc le même espace d'adressage virtuel (!) (Variables globales, texte, données et segments de tas)
- Le segment de pile des processus « stack » est divisé en « ministack » pour chaque thread.
- Il existe de nombreuses similitudes entre les threads et les processus ; en fait, les threads sont souvent appelés processus légers "lightweight processes" .
Multithreading
Processus:
- isoler les espaces d'adressage virtuels (bon : sécurité et stabilité, mauvais : partage d'informations plus difficile)
- peut exécuter facilement des programmes externes (fork-exec) (bien)
- plus difficile de coordonner plusieurs tâches au sein du même programme (mauvais)
Threads:
- partager l'espace d'adressage virtuel (mauvais : sécurité et stabilité, bon : partage d'informations plus facile)
- ne peut pas exécuter facilement des programmes externes (mauvais)
- plus facile de coordonner plusieurs tâches au sein d'un même programme (bien)
Threads vs. Processus
C thread
Un thread peut être généré pour exécuter la fonction spécifiée avec les arguments donnés.
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
//Compile and link with -pthread.- La fonction pthread_create() démarre un nouveau thread dans le processus appelant. Le nouveau thread démarre l'exécution en appelant start_routine(); arg est passé comme seul argument de start_routine().
- start_routine(): la fonction que le thread doit exécuter de manière asynchrone
- arg: une liste d'arguments (zéro ou n'importe quelle longueur ) à passer à la fonction lors de l'exécution
- Une fois créer le thread peut s'exécuter à tout moment !
C thread
Pour plusieurs threads, nous devons attendre un thread spécifique à la fois :
thread treads[5];
...
for (size_t i = 0; i < 5; i++) {
pthread_join(treads[i], NULL);Pour attendre la fin d'un thread, utilisez la fonction pthread_join :
static void * threadFunc(void *arg)
{// instructions
return NULL;
}
//
pthread_t mythread;
pthread_create(&mythread, NULL,threadFunc, NULL);
... // do some work
// Wait for thread to finish (blocks)
pthread_join(mythread, NULL);C thread
Le nouveau thread se termine de l'une des manières suivantes :
-
Il appelle pthread_exit(3), en spécifiant une valeur d'état de sortie qui est disponible pour un autre thread dans le même processus qui appelle pthread_join(3).
-
Il revient de start_routine(). Cela équivaut à appeler pthread_exit(3) avec la valeur fournie dans l'instruction return. -
Il est annulé (voir pthread_cancel(3)). -
L'un des threads du processus appelle exit(3), ou le thread principal effectue un retour depuis main(). Cela provoque la fin de tous les threads du processus.
Terminaison d'un thread:
Notre premier programme Thread
#include <stdio.h>
#include <pthread.h> // for C thread support
static const int kNumFriends = 6;
static void * /* Loop 'arg' times incrementing 'glob' */
greeting(void *arg)
{
printf("Hello, world!\n");
return NULL;
}
int main(int argc, char *argv[]) {
printf("Let's hear from %d threads \n",kNumFriends);
// declare array of empty thread handles
pthread_t friends[kNumFriends];
// Spawn threads
for (size_t i = 0; i < kNumFriends; i++) {
pthread_create(&friends[i], NULL, greeting, NULL);
}
// Wait for threads
for (size_t i = 0; i < kNumFriends; i++) {
pthread_join(friends[i], NULL);
}
printf("Everyone's said hello!\n");
return 0;
}
Our First Threads Program
Our First Threads Program
#include <stdio.h>
#include <pthread.h> // for C thread support
static const int kNumFriends = 6;
static void * /* Loop 'arg' times incrementing 'glob' */
greeting(void *arg)
{
int * iptr =(int*)arg;
printf("Hello, world! I am thread %i \n",*iptr);
return NULL;
}
int main(int argc, char *argv[]) {
printf("Let's hear from %d threads \n",kNumFriends);
// declare array of empty thread handles
pthread_t friends[kNumFriends];
// Spawn threads
for (size_t i = 0; i < kNumFriends; i++) {
pthread_create(&friends[i], NULL, greeting, &i);
}
// Wait for threads
for (size_t i = 0; i < kNumFriends; i++) {
pthread_join(friends[i], NULL);
}
printf("Everyone's said hello!\n");
return 0;
}
Our First Threads Program
C thread
// declare array of empty thread handles
pthread_t friends[kNumFriends];
// Spawn threads
for (size_t i = 0; i < kNumFriends; i++) {
pthread_create(&friends[i], NULL, greeting, &i);
}Nous pouvons créer un tableau de threads comme suit :
Race Conditions (Conditions de course)
- Comme pour les processus, les threads peuvent s’exécuter dans des ordres imprévisibles.
- Une condition de course est un ordre imprévisible d'évènements où certains ordres peuvent provoquer un comportement indésirable.
- Une fonction thread-safe est une fonction qui s'exécutera toujours correctement, même lorsqu'elle est appelée simultanément à partir de plusieurs threads.
- printf n'est pas une fonction thread-safe. Cela signifie par exemple que les affichage peuvent être entrelacées !
Threads Program(en c++)
static const size_t kNumFriends = 6;
static void greeting(size_t i) {
cout << oslock << "Hello, world! I am thread " << i << endl << osunlock;
}
int main(int argc, char *argv[]) {
cout << "Let's hear from " << kNumFriends << " threads." << endl;
// declare array of empty thread handles
thread friends[kNumFriends];
// Spawn threads
for (size_t i = 0; i < kNumFriends; i++) {
friends[i] = thread(greeting, i);
}
// Wait for threads
for (size_t i = 0; i < kNumFriends; i++) {
friends[i].join();
}
cout << "Everyone's said hello!" << endl;
return 0;
}
Les threads partagent la mémoire
- Contrairement aux processus parents/enfants, les threads s'exécutent dans le même espace d'adressage virtuel!
- Cela signifie que nous pouvons par exemple transmettre des paramètres par référence et permettre à tous les threads d'y accéder/de les modifier !
- Pour passer une valeur par référence avec pthread_create, nous devons utiliser le quatrième arguments:
greeting(void *arg)
{
int * iptr =(int*)arg;
...
}
// Spawn threads
for (size_t i = 0; i < kNumFriends; i++) {
pthread_create(&friends[i], NULL, greeting, &i);
}Threads Share Memory
for (size_t i = 0; i < kNumFriends; i++) {
pthread_create(&friends[i], NULL, greeting, &i);
}_start
greeting
main
argc
argv
i
args
args
args
args
args
args
piles de threads
main stack
Ici, nous pouvons simplement passer par copie. Mais faites attention aux conséquences de la mémoire partagée !
Threads Share Memory
- Les threads permettent à un processus de paralléliser un problème sur plusieurs cores
- Considérons un scénario où nous voulons vendre 250 billets et disposons de 10 cœurs
- Simulation : laissez chaque thread aider à vendre des tickets jusqu'à ce qu'il n'en reste plus
int main(int argc, const char *argv[]) {
thread ticketAgents[kNumTicketAgents];
size_t remainingTickets = 250;
for (size_t i = 0; i < kNumTicketAgents; i++) {
ticketAgents[i] = thread(sellTickets, i, ref(remainingTickets));
}
for (thread& ticketAgent: ticketAgents) {
ticketAgent.join();
}
cout << "Ticket selling done!" << endl;
return 0;
}Parallélisme au niveau des threads

Demo: confused-ticket-agents.cc

- There is a race condition in this code caused by multiple threads accessing remainingTickets.
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
cout << oslock << "Thread #" << id << " sold a ticket (" << remainingTickets
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread #" << id << " sees no remaining tickets to sell and exits."
<< endl << osunlock;
}Overselling Tickets
thread #1
thread #2
thread #3
remainingTickets = 1
- There is a race condition in this code caused by multiple threads accessing remainingTickets.
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
cout << oslock << "Thread #" << id << " sold a ticket (" << remainingTickets
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread #" << id << " sees no remaining tickets to sell and exits."
<< endl << osunlock;
}Overselling Tickets
thread #1
thread #2
thread #3
Line 2: checking if there are tickets left. Yep!
remainingTickets = 1
- There is a race condition in this code caused by multiple threads accessing remainingTickets.
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
cout << oslock << "Thread #" << id << " sold a ticket (" << remainingTickets
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread #" << id << " sees no remaining tickets to sell and exits."
<< endl << osunlock;
}Overselling Tickets
thread #1
thread #2
thread #3
Line 2: checking if there are tickets left. Yep!
remainingTickets = 1
z
z
z
- There is a race condition in this code caused by multiple threads accessing remainingTickets.
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
cout << oslock << "Thread #" << id << " sold a ticket (" << remainingTickets
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread #" << id << " sees no remaining tickets to sell and exits."
<< endl << osunlock;
}Overselling Tickets
thread #1
thread #2
thread #3
Line 2: checking if there are tickets left. Yep!
remainingTickets = 1
z
z
z
z
z
z
- There is a race condition in this code caused by multiple threads accessing remainingTickets.
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
cout << oslock << "Thread #" << id << " sold a ticket (" << remainingTickets
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread #" << id << " sees no remaining tickets to sell and exits."
<< endl << osunlock;
}Overselling Tickets
thread #1
thread #2
thread #3
Line 4: Selling ticket!
remainingTickets = 0
z
z
z
z
z
z
- There is a race condition in this code caused by multiple threads accessing remainingTickets.
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
cout << oslock << "Thread #" << id << " sold a ticket (" << remainingTickets
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread #" << id << " sees no remaining tickets to sell and exits."
<< endl << osunlock;
}Overselling Tickets
thread #1
thread #2
thread #3
Line 4: Selling ticket!
remainingTickets = <really large number>
z
z
z
- There is a race condition in this code caused by multiple threads accessing remainingTickets.
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
cout << oslock << "Thread #" << id << " sold a ticket (" << remainingTickets
<< " remain)." << endl << osunlock;
}
cout << oslock << "Thread #" << id << " sees no remaining tickets to sell and exits."
<< endl << osunlock;
}Overselling Tickets
thread #1
thread #2
thread #3
Line 4: Selling ticket!
remainingTickets = <really large number - 1>
There is a race condition here!
- Problem: threads could interrupt each other in between checking tickets and selling them.
- If a thread evaluates remainingTickets > 0 to be true and commits to selling a ticket, another thread could come in and sell that same ticket before this thread does.
- This can happen because remainingImages > 0 test and remainingImages-- aren't atomic.
- Atomicity: externally, the code has either executed or not; external observers do not see any intermediate states mid-execution.
- We want a thread to do the entire check-and-sell operation uninterrupted.
Overselling Tickets
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets > 0) {
sleep_for(500); // simulate "selling a ticket"
remainingTickets--;
...
}- C++ statements aren't inherently atomic.
- We assume that assembly instructions are atomic; but even single C++ statements like remainingTickets-- take multiple assembly instructions.
Atomicity
- Even if we altered the code to be something like this, it still wouldn't fix the problem:
static void sellTickets(size_t id, size_t& remainingTickets) {
while (remainingTickets-- > 0) {
sleep_for(500); // simulate "selling a ticket"
...
}// gets remainingTickets
0x0000000000401a9b <+36>: mov -0x20(%rbp),%rax
0x0000000000401a9f <+40>: mov (%rax),%eax
// Decrements by 1
0x0000000000401aa1 <+42>: lea -0x1(%rax),%edx
// Saves updated value
0x0000000000401aa4 <+45>: mov -0x20(%rbp),%rax
0x0000000000401aa8 <+49>: mov %edx,(%rax)- Each core has its own registers that it has to read from
- Each thread makes a local copy of the variable before operating on it
- Problem: What if multiple threads do this simultaneously? They all think there's only 128 tickets remaining and process #128 at the same time!
Atomicity
// gets remainingImages
0x0000000000401a9b <+36>: mov -0x20(%rbp),%rax
0x0000000000401a9f <+40>: mov (%rax),%eax
// Decrements by 1
0x0000000000401aa1 <+42>: lea -0x1(%rax),%edx
// Saves updated value
0x0000000000401aa4 <+45>: mov -0x20(%rbp),%rax
0x0000000000401aa8 <+49>: mov %edx,(%rax)It would be nice if we could put the check-and-sell operation behind a "locked door" and say "only one thread may enter at a time to do this block of code".
Recap
- Introducing multithreading
- Example: greeting friends
- Race conditions
- Threads share memory
- Completing tasks in parallel
- Example: selling tickets
Next time: introducing mutexes
Introduction to Multithreading
By NadGy
Introduction to Multithreading
- 36