Académique Documents
Professionnel Documents
Culture Documents
A. ZAKARIA
2020/2021
Sommaire
Introduction
Multitasking vs multithreading
C++11 threading support library
Création de Thread
Synchronization
L'utilisation de Mutex et ses problèmes
Condition variable
Design patterns
Producer/Consumer
Active Object
Reactor
ThreadPool
Introduction
Parallélisme
Effectuer des opérations simultanément (en parallèle)
Nous pouvons marcher, parler, respirer, voir, entendre, sentir... tout en
même temps.
Les ordinateurs peuvent également le faire - télécharger un fichier,
imprimer un fichier, recevoir des courriels, faire tourner l'horloge, plus ou
moins en parallèle....
Comment ces tâches sont-elles généralement accomplies ?
Les systèmes d'exploitation prennent en charge les processus
Quelle est la différence entre un processus et un thread ?
Les processus ont leur propre espace mémoire, les threads partagent la
mémoire
Ainsi, les processus sont "lourds" tandis que les threads sont "légers".
Introduction
Quoi et pourquoi ?
Threads d ’execution
Chaque thread est une partie d'un programme qui peut s'exécuter simultanément
avec d'autres threads (Multithreading)
Exemple : téléchargement d'un clip vidéo
Au lieu de devoir télécharger le clip en entier, vous pouvez le lire :
Téléchargez une partie, jouez cette partie, téléchargez la partie suivante, jouez cette partie...
(streaming)
Il faut que cela se fasse sans heurts (smoothly)
….
Introduction
Multitasking vs multithreading:
Les systèmes d'exploitation multitâches(Multitasking) permettent
d'exécuter plusieurs "processus" simultanément
Une seule demande peut engendrer plusieurs processus
Le système d'exploitation attribue un espace mémoire distinct à
chaque processus (un processus ne peut pas accéder directement
à l'espace mémoire d'un autre processus)
Le multithreading permet à un seul processus d'exécuter T1 T2
plusieurs tâches simultanément (dans un espace mémoire
partagé)
Partage rapide et facile des structures de données entre les tâches
T1 T2
Introduction
Pourquoi le multithreading ?
Permettre à une application d'effectuer plusieurs tâches simultanément
Par exemple, l'utilisation d'une interface utilisateur réactive, la gestion des communications sur le
réseau et l'exécution simultanée de calculs lourds par lots
Assistance multithreading
Côté matériel : nous sommes actuellement à l'ère du multicore (et du manycore)
Des performances supplémentaires des architectures de calcul fournies par un nombre croissant de
cœurs
Process
Registers Registers
Processor
Sommaire
Introduction
Multitasking vs multithreading
C++11 threading support library
Création de Thread
Synchronization
L'utilisation de Mutex et ses problèmes
Condition variable
Design patterns
Producer/Consumer
Active Object
Reactor
ThreadPool
C++11 multithreading support
Class std::thread
Le constructeur lance un thread exécutant la fonction donnée
#include <iostream>
#include <thread> using
namespace std;
using namespace std::chrono;
void myThread() {
for(;;) {
cout<<"world "<<endl;
this_thread::sleep_for(milliseconds(500));
}
}
int main() {
thread t(myThread);
for(;;) {
cout<<"hello"<<endl;
this_thread::sleep_for(milliseconds(500));
}
}
C++11 multithreading support
Class std::thread
$ g++ main.cpp -o test -std=c++11 -pthread
helloworld
hello
world
helloworld
hello
world
helloworld
C++11 multithreading support
Class std::thread
Le constructeur de thread peut prendre des arguments supplémentaires
qui sont passés à la fonction thread
La fonction membre "join()" attend que le thread soit terminé
#include <iostream>
#include <thread>
using namespace std;
int main() {
thread t(myThread, 2, "test");
t.join();
}
C++11 multithreading support
Synchronisation
Quel est le résultat du code suivant ?
#include <iostream>
#include <thread>
using namespace std;
static int sharedVariable=0;
void myThread() {
for(int i=0;i<1000000;i++) sharedVariable++;
}
int main() {
thread t(myThread);
for(int i=0;i<1000000;i++) sharedVariable--;
t.join();
cout<<"sharedVariable="<<sharedVariable<<endl;
}
C++11 multithreading support
Synchronisation
$ ./test
sharedVariable=-313096
$ ./test
sharedVariable=-995577
$ ./test
sharedVariable=117047
$ ./test
sharedVariable=116940
$ ./test
sharedVariable=-647018
//sharedVariable--
movl sharedVariable(%rip), %eax
subl $1, %eax
movl %eax, sharedVariable(%rip)
Les incréments (++) et les décréments (--) ne sont pas des opérations
atomiques
Le système d'exploitation peut anticiper un thread entre n'importe quelle
instruction
C++11 multithreading support
Synchronisation
Que se passe-t-il sous le capot ?
#include <iostream>
#include <thread> int main() {
#include <mutex> thread t(myThread);
using namespace std; for(int i=0;i<1000000;i++) {
static int sharedVariable=0; myMutex.lock();
mutex myMutex; sharedVariable--;
myMutex.unlock();
void myThread() { }
for(int i=0;i<1000000;i++) { t.join();
myMutex.lock(); cout<<"sharedVariable="
sharedVariable++; <<sharedVariable<<endl;
myMutex.unlock(); }
}
}
problèmes?
C++11 multithreading support
Deadlock
Une utilisation incorrecte du mutex peut conduire à un
deadlock, selon lequel l'exécution du programme se bloque
Les Deadlocks peuvent être dus à plusieurs causes
Cause 1 : oubli de déverrouiller un mutex
…
mutex myMutex;
int sharedVariable;
void func2()
{
lock_guard<mutex> lck(myMutex); Deadlock si c'est
doSomething2(); appelé par func1()
}
void func1()
{
lock_guard<mutex> lck(myMutex);
doSomething1();
func2(); Appelé avec le
} mutex verrouillé
C++11 multithreading support
Deadlock
Solution : Le mutex récursif (Recursive mutex) permet de
multiples verrouillages par le même thread
…
recursive_mutex myMutex;
int sharedVariable;
void func2() {
lock_guard<recursive_mutex> lck(myMutex);
doSomething2();
}
void func1(){
lock_guard<recursive_mutex> lck(myMutex);
doSomething1();
func2();
}
Cependant, les mutex récursifs sont plus coûteux que les mutex.
Il faut l’utiliser uniquement en cas de besoin
C++11 multithreading support
Deadlock
Cause 3 : Ordre de verrouillage de plusieurs mutex
…
mutex myMutex1;
mutex myMutex2;
void func2() {
lock_guard<mutex> lck1(myMutex1);
lock_guard<mutex> lck2(myMutex2);
doSomething2();
}
void func1(){
lock_guard<mutex> lck1(myMutex2);
lock_guard<mutex> lck2(myMutex1);
doSomething1();
}
mutex myMutex1;
mutex myMutex2;
void func2() {
lock(myMutex1,myMutex2);
doSomething2();
myMutex1.unlock();
myMutex2.unlock();
}
void func1(){
lock(myMutex2,myMutex1);
doSomething1();
myMutex2.unlock();
myMutex1.unlock();
}
Un nombre quelconque de mutex peut être passé au lock() et dans n'importe quel ordre.
L'utilisation de lock est plus coûteuse que celle de lock_guard
C++11 multithreading support
Deadlock & Race conditions
Sont des défauts qui surviennent en raison d'un ordre d'exécution "inattendu" des
threads.
Les programmes corrects doivent fonctionner quel que soit l'ordre d'exécution
L'ordre qui déclenche la faute peut être extrêmement rare
Le fait de lancer le même programme des millions de fois peut ne pas déclencher le
défaut.
sont donc difficiles à déboguer
Il est difficile de reproduire le bug
Les tests sont presque inutiles pour vérifier ces erreurs
Une bonne conception est indispensable!
C++11 multithreading support
Perte de la concurrence
Le fait de laisser un mutex verrouillé pendant une longue période réduit la
concurrence dans le programme
mutex myMutex;
int sharedVariable=0;
void myFunction()
{
lock_guard<mutex> lck(myMutex);
sharedVariable++;
this_thread::sleep_for(milliseconds(500));
}
C++11 multithreading support
Perte de la concurrence
Solution : Les sections critiques doivent être aussi courtes que possible
Laissez les opérations inutiles en dehors de la section critique
…
mutex myMutex;
int sharedVariable=0;
void myFunction()
{
{
lock_guard<mutex> lck(myMutex);
sharedVariable++;
}
this_thread::sleep_for(milliseconds(500));
}
C++11 multithreading support
Condition variable
Dans de nombreux programmes multithreads, nous pouvons avoir des
dépendances entre les threads
Une "dépendance" peut venir du fait que le thread doit attendre qu'un autre
thread termine son opération en cours
Dans un tel cas, nous avons besoin d'un mécanisme pour bloquer explicitement un
thread.
class std::condition_variable
Fonction de trois membres
wait(unique_lock<mutex> &) bloque le thread jusqu'à ce qu'un autre thread le
réveille.
Le mutex est débloqué pour la durée de wait(...)
notify_one() réveille l'un des threads en attente
notify_all() réveille tous les threads en attente
Si aucun thread n'est en attente, on ne fait rien
C++11 multithreading support
class std::condition_variable
Dans l'exemple, myThread attend que la fonction main() termine la lecture à partir
de l'entrée standard.
#include <iostream>
#include <thread> int main() {
#include <mutex> thread t(myThread); string s;
#include <condition_variable> cin>>s; // read from stdin
using namespace std; {
string shared; unique_lock<mutex> lck(myMutex);
mutex myMutex; shared=s;
condition_variable myCv; myCv.notify_one();
}
void myThread() { t.join();
unique_lock<mutex> lck(myMutex); }
if(shared.empty()) myCv.wait(lck);
cout<<shared<<endl;
}
Sommaire
Introduction
Multitasking vs multithreading
C++11 threading support library
Création de Thread
Synchronization
L'utilisation de Mutex et ses problèmes
Condition variable
Design patterns
Producer/Consumer
Active Object
Reactor
ThreadPool
Design patterns
Producer/Consumer (producteur/consommateur)
Producer Consumer
Thread Thread
Queue
Un thread (consommateur) a besoin de données d'un autre thread (producteur)
Pour découpler les opérations des deux threads, on met une queue entre eux, pour
mettre en mémoire tampon(buffer) les données si le producteur est plus rapide
que le consommateur.
L'accès à la queue doit être synchronisé
Non seulement le consommateur utilise un mutex, mais il doit attendre si la queue est vide
Facultativement, le producteur peut bloquer si la queue est pleine
Design patterns
Producer/Consumer (producteur/consommateur)
synchronized_queue.h (1/2)
#ifndef SYNC_QUEUE_H_
#define SYNC_QUEUE_H_
#include <list>
#include <mutex>
#include <condition_variable>
template<typename T>
class SynchronizedQueue {
public:
SynchronizedQueue(){}
void put(const T & data);
T get();
private:
SynchronizedQueue( const SynchronizedQueue &);
SynchronizedQueue & operator=(const SynchronizedQueue &);
std::list<T> queue;
std::mutex myMutex;
std::conditionVariable myCv;
};
…
Design patterns
Producer/Consumer (producteur/consommateur)
synchronized_queue.h (2/2)
template<typename T>
void SynchronizedQueue<T>::put(const T& data)
{
std::unique_lock<std::mutex> lck(myMutex);
queue.push_back(data);
myCv.notify_one();
}
template<typename T>
T SynchronizedQueue<T>::get()
{
std::unique_lock<std::mutex> lck(myMutex);
while(queue.empty())
myCv.wait(lck);
T result=queue.front();
queue.pop_front();
return result;
}
#endif // SYNC_QUEUE_H_
Design patterns
Producer/Consumer (producteur/consommateur)
main.cpp
#include “synchronized_queue.h”
#include <iostream>
#include <thread>
using namespace std;
using namespace std::chrono;
SynchronizedQueue<int> queue;
void myThread() {
for(;;) cout<<queue.get()<<endl;
}
int main() {
thread t(myThread);
for(int i=0;;i++) {
queue.put(i);
this_thread::sleep_for(seconds(1));
}
}
Design patterns
Producer/Consumer (producteur/consommateur)
Que se passe-t-il si on n'utilise pas la variable de condition ?
synchronized_queue.h (1/2)
#ifndef SYNC_QUEUE_H_
#define SYNC_QUEUE_H_
#include <list>
#include <mutex>
template<typename T>
class SynchronizedQueue {
public:
SynchronizedQueue(){}
void put(const T & data);
T get();
private:
SynchronizedQueue( const SynchronizedQueue &);
SynchronizedQueue & operator=(const SynchronizedQueue &);
std::list<T> queue;
std::mutex myMutex;
// std::conditionVariable myCv;
};
…
Design patterns
Producer/Consumer (producteur/consommateur)
synchronized_queue.h (2/2)
template<typename T>
void SynchronizedQueue<T>::put(const T & data)
{
std::unique_lock<std::mutex> lck(myMutex);
queue.push_back(data);
//myCv.notify_one();
}
template<typename T>
T SynchronizedQueue<T>::get() {
for(;;) {
std::unique_lock<std::mutex> lck(myMutex);
if(queue.empty()) continue;
T result=queue.front();
queue.pop_front();
return result;
}
}
#endif // SYNC_QUEUE_H_
Design patterns
Producteur/Consommateur
Que se passe-t-il si on n'utilise pas la variable de condition ?
Le consommateur est laissé "à la dérive" lorsque la queue est vide
Cela occupe de précieux cycles de CPU et ralentit d'autres threads du système
Le fait de garder le CPU occupé augmente la consommation d'énergie
Bien que le code soit correct d'un point de vue fonctionnel, il s'agit d'une mauvaise
approche de programmation
Lorsqu'un thread n'a rien à faire, il doit se bloquer pour libérer le CPU pour d'autres
threads et réduire la consommation d'énergie
ActiveObject::ActiveObject():
t(bind(&ActiveObject::run, this)), quit(false) {}
void ActiveObject::run() {
while(!quit.load()) {
cout<<"Hello world"<<endl;
this_thread::sleep_for(milliseconds(500));
}
}
ActiveObject::~ActiveObject() {
if(quit.load()) return; //For derived classes
quit.store(true);
t.join();
}
Design patterns
Active Object
Le constructeur initialise l'objet thread, tandis que le destructeur se charge de le
joindre
La fonction membre run()agit comme un " main " exécutant simultanément
Généralement utilisé pour implémenter des threads communiquant à travers une
approche producteur/consommateur
Dans l'exemple d'implémentation, nous avons utilisé la variable "atomique" quit
pour terminer la fonction run() lorsque l'objet est détruit.
Une variable booléenne normale avec un mutex fonctionnerait également
Design patterns
bind and function
En C++11, bind et function nous permettent de regrouper une fonction et ses
arguments, et de l'appeler plus tard
#include <iostream>
#include <functional>
using namespace std;
int main() {
function<void ()> func; On spécifie les arguments de la
func = bind(&printAdd, 2, 3); fonction sans effectuer l'appel.
…
func(); L'appel de fonction (avec des
} arguments déjà packagés)
Design patterns
Reactor
L'objectif est de découpler la création de tâches de l'exécution
Un thread exécuteur attend dans une queue de tâches
Toute autre partie du programme peut faire passer des tâches dans la queue.
Les tâches sont exécutées de manière séquentielle
La solution la plus simple est généralement de passer par la méthode FIFO
On est libre d'ajouter au « reactor » des fonctions alternatives d'ordonnancement
(scheduling) des threads
C++11: bind et function nous permet de créer la tâche, en laissant le temps de
démarrage au thread exécuteur, dans un second temps
Design patterns
Reactor
La classe dérive de la classe ActiveObject pour implémenter le thread exécuteur
et utilise la SynchronizedQueue pour la queue des tâches
#ifndef REACTOR_H_
#define REACTOR_H_
#include <functional>
#include “synchronized_queue.h”
#include “active_object.h”
void doNothing() {}
Reactor::~Reactor() {
quit.store(true);
pushTask(&doNothing);
t.join(); // Thread derived from ActiveObject
}
void Reactor::run() {
while(!quit.load())
tasks.get()(); // Get a function and call it
}
Design patterns
Reactor
Dans l'exemple, on pousse une tâche pour exécuter la fonction printAdd()
#include <iostream>
int main()
{
Reactor reac;
reac.pushTask(bind(&printAdd,2,3));
…
}
Design patterns
ThreadPool
La limite du pattern Reactor est due au fait que les tâches sont traitées de manière
séquentielle
La latence de l'exécution de la tâche dépend de la longueur de la queue des tâches.