Programmation multi-threads en C++ 

Malgré le gain de popularité de la programmation multi-threads et cela dès le début des années 90, le comité ANSI/ISO a choisi de ne pas en tenir compte lors de la refonte de la norme C++ en 1998. Ainsi jusqu’à il y a quelques années, les programmeurs C++ désirant manipuler les threads avaient recours à des extensions spécifiques aux plate-formes [52, 62], le plus souvent implémentées comme surcouche de Pthreads ou de Windows threads. Conscient de l’importance de ce modèle de programmation, accentuée par la mise sur le marché des processeurs multi-cœurs, le comité de normalisation du langage C++ a décidé en 2011 d’inclure de nouveaux composants permettant à la fois de faciliter la programmation multi-threads et à la fois d’écrire du code parallèle portable qui puisse fonctionner sur différentes plate-formes.

Gestion des threads

Tout programme décrit en C++ lance au moins un thread : celui qui exécute la fonction main(). Un programme peut ensuite lancer d’autres threads en leur spécifiant d’autres points d’entrée. Ces threads vont alors s’exécuter en concurrence avec le thread initial. Voici un exemple de code montrant comment lancer un thread avec la nouvelle norme C++:

void f(std::string const & s)
{
std::cout <<  » Hello  » << s << std::endl;
}
std::string mot =  » World ! »;
std::thread t (&f,mot); 
/* … */
t.join();

Listing 1.1: Lancer un thread et attendre celui-ci

Une des fonctionnalités intéressantes de la norme C++ 2011 est de pouvoir définir des fonctions locales anonymes capables de capturer des variables dans leur portée : ce sont les fonctions lambda. La syntaxe est la suivante:

[ liste de capture ]( paramètres de la fonction ) -> type de retour
{ corps de la fonction }

Une fois le thread lancé, l’utilisateur doit s’assurer, ou bien d’appeler la méthode join() pour attendre que le thread se termine, ou bien, d’appeler la méthode detach() pour permettre au thread de s’exécuter en arrière-plan, et cela avant que l’instance de std::thread ne soit détruite. Dans le cas contraire, c’est le destructeur de la classe std::thread qui se charge d’interrompre le programme entier. La méthode join() est la manière la plus simple d’attendre qu’un thread ait fini son travail, mais ce mécanisme reste un peu faible en terme d’efficacité et a surtout le défaut de n’être invocable qu’une fois, car en sortie d’appel à join(), le thread système sous-jacent aura disparu.

Gestion des accès aux données

Pour revenir sur notre propos, la raison clé qui incite une bonne majorité des programmeurs à utiliser le modèle de programmation multi-thread est la simplicité du mode de communication entre threads vu qu’ils partagent le même espace mémoire. Il faut toutefois faire attention aux accès concurrents aux données partagées et donc dans la plupart des cas, avoir recours aux mutexes (ou exclusions mutuelles). Voici un exemple de code illustrant l’utilisation des mutexes avec la nouvelle norme C++:

std::vector <int > v;
std::mutex m; 
void ajouter_valeur( int valeur)
{
m.lock(); 
v.push_back(valeur);
m.unlock(); 
}
std::thread t1 (& ajouter_valeur , 1);
std::thread t2 (& ajouter_valeur , 2);

Listing 1.3: Protéger l’accès concurrent à une donnée

Ici le mutex est créé au moment où la variable m est instanciée  . Une section critique débute alors en appelant la méthode lock() de l’objet std::mutex et se termine en appelant la méthode unlock() . Pour plus de sûreté et une gestion efficace des éventuelles exceptions, la nouvelle norme C++ recommande d’utiliser des objets verrous par l’intermédiaire des classes template std::lock_guard et std::unique_lock . Conformément à la notion d’Acquisition de Ressources par l’Initialisation (ou RAII en anglais), c’est au moment de la création de l’objet verrou que le mutex est verrouillé et au moment de sa destruction qu’il est déverrouillé. Ces classes C++ s’avèrent pratiques vu qu’il arrive souvent que la portée des corps de fonctions coïncident avec les sections critiques désirées. De plus, si une exception devait survenir dans la portée de ces objets, le mécanisme de stack unwinding avant retour de la fonction appellera automatiquement les destructeurs de ces objets. Les ressources acquises jusqu’ici seront donc proprement restituées quoi qu’il arrive. Le code précédent se réécrit donc comme ci-dessous:

void ajouter_valeur( int valeur)
{
std::lock_guard <std::mutex > l(m);
v.push_back(valeur);
}

Listing 1.4: Protéger un accès concurrent par verrou

Ayant présenté les fondamentaux de la programmation multi-threads, nous discutons dans la suite de la problématique de synchronisation des threads dans le cas très particulier où il s’agit de faire en sorte qu’un thread attende qu’un autre thread ait fini une opération (et non pas qu’il se termine) pour pouvoir continuer.

Le rapport de stage ou le pfe est un document d’analyse, de synthèse et d’évaluation de votre apprentissage, c’est pour cela chatpfe.com propose le téléchargement des modèles complet de projet de fin d’étude, rapport de stage, mémoire, pfe, thèse, pour connaître la méthodologie à avoir et savoir comment construire les parties d’un projet de fin d’étude.

Table des matières

Introduction
Parallélisme et concurrence
Les défis de la programmation parallèle
NT2 : Une bibliothèque pour le calcul scientifique
Objectifs
Mettre en œuvre des tâches légères pour le calcul scientifique
Intégrer des modèles de coût pour la génération automatique de code
Notre contribution
1 Programmation multi-threads en C++ 
1.1 Gestion des threads
1.2 Gestion des accès aux données
1.3 Synchronisation sur événement ponctuel
1.3.1 Synchronisation par variables conditionnelles
1.3.2 Synchronisation par Futures
1.4 Des threads aux tâches asynchrones
1.5 Concurrence par tâches légères
1.5.1 Gestion des files d’attente de tâches
1.5.2 Modèles de tâches légères et graphes de dépendances
2 Expression Templates et Squelettes Algorithmiques orientés domaine
2.1 Application des Expression Templates pour la gestion statique d’ASTs
2.2 Des ASTs aux squelettes algorithmiques
2.3 Une API asynchrone pour l’invocation des tâches légères
2.3.1 HPX – Un runtime pour des applications parallèles et distribuées
2.3.2 Composition séquentielle des Futures
2.3.3 Composition parallèle des Futures
3 Génération automatique de code concurrent pour un langage enfoui orienté domaine
3.1 Intégration des Futures dans NT2
3.2 Résolution des aléas de données
3.3 Travaux associés
4 Modèles de coût pour la génération de code
4.1 Modèles analytiques de performance pour les multi-cœurs
4.1.1 Analyses de performance basées sur les contraintes
4.1.2 Coûts orientés temps d’exécution
4.1.3 Des frameworks de squelettes orientés modèles de coût
4.2 Modèles de coût pour la paramétrisation de squelettes
4.2.1 Métriques de ressource portables pour des squelettes réalistes
4.2.2 Des arbres de syntaxe abstraits aux prédictions fondés sur la connaissance de l’application
4.2.3 Avantages et inconvénients d’une approche par modèles de coût statiques
5 Évaluation de performances
5.1 Décomposition LU – version tuilée
5.2 Black & Scholes
5.3 Lattice Boltzmann – version D2Q9
5.4 GMRES
5.5 Synthèse des résultats obtenus
Conclusions

Lire le rapport complet

Télécharger aussi :

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *