Blog Alphorm Logo de blog informatique spécialisé en technologie et solutions IT
  • Développement
  • 3D et Animation
  • Cybersécurité
  • Infrastructure
  • Virtualisation
  • Réseaux
  • Bureautique
  • BDD
En cours de lecture : La concurrence en C++
Agrandisseur de policeAa
Blog AlphormBlog Alphorm
  • Développement
  • 3D et Animation
  • Cybersécurité
  • Infrastructure
  • Virtualisation
  • Réseaux
  • Bureautique
  • BDD
Search
  • Développement
  • 3D et Animation
  • Cybersécurité
  • Infrastructure
  • Virtualisation
  • Réseaux
  • Bureautique
  • BDD
Suivez-nous
© Alphorm 2024 - Tous droits réservés
CybersécuritéDéveloppement

La concurrence en C++

Fabien Brissonneau Par Fabien Brissonneau 18 janvier 2025
Partager
7e lecture en min
Partager

Généralités

La gestion de la concurrence peut être réalisée à 3 niveaux : bas niveau, via des variables atomiques, qui se manipule sans possibilités d’accès simultanés, avec les threads, où finalement, le programmeur traite directement des threads systèmes, et enfin via les tâches, un concept bien plus proche de l’objectif du développeur en général.

La gestion bas-niveau de la concurrence

La classe atomic<T> représente un type sur lequel les opérations sont atomiques, c’est-à-dire qu’elles s’exécutent dans un seul thread sans interférences avec un autre. Les spécialisations de atomic peuvent concerner les types fondamentaux, et l’implémentation peut varier. Les différentes fonctions membres proposées sur atomic suppose que vous écriviez un code très bas niveau qui, dans bien des cas, peut être remplacé par des abstractions bien plus simples à mettre en place.

Les atomic_flags sont des types très simples, qui sont garantis d’opérations atomiques. Les fonctions disponibles sont essentiellement set() et clear(), permettant de connaître et de positionner l’état de ce flag.

Table de matière
GénéralitésLa gestion bas-niveau de la concurrenceLa gestion de threadsLa gestion des tâchesConclusion

Le mot-clé volatile permet de son côté d’indiquer qu’une variable peut être modifiée de l’extérieur du thread dans lequel elle est déclarée. Cela évite la réorganisation des opérations de lecture/écriture.

La gestion de threads

Les threads du C++ sont prévus pour mapper un à un les threads systèmes. Les threads possèdent en propre leurs piles, mais partagent la mémoire du tas. Les variables partagées sont donc susceptibles de concurrences malheureuses entre threads.

#include
<thread>

Penser à inclure la bibliotheque thread.

std::cout << « thread courant « <<std::this_thread::get_id();

Permet de récupérer l’id du thread courant.

Pour lancer les threads, il suffit de passer une fonction, éventuellement des arguments.

std::thread t1(ma_fonction);

std::thread t2(ma_fonction2);

t1.join();

t2.join();

std::cout << « FIN du thread «  << std::this_thread::get_id()<<std::endl;

Dans ce code, 2 threads sont créés, utilisant 2 fonctions qui font juste un affichage et attendent un peu. Puis le thread principal est bloqué.

void ma_fonction() {

std::chrono::duration<int> duree(1);

for (int i = 0; i < 10; i++) {

std::cout << « thread courant «  << std::this_thread::get_id()<<std::endl;

std::this_thread::sleep_for(duree);

}

}

void ma_fonction2() {

std::chrono::duration<int> duree(3);

for (int i = 0; i < 10; i++) {

std::cout << « thread courant «  << std::this_thread::get_id() << std::endl;

std::this_thread::sleep_for(duree);

}

}

Le résultat dépend de l’ordonnancement des threads, le join() garantit que le main se terminera lorsque les deux threads seront terminés.

Pour éviter les « data races », soit les accès concurrents malheureux sur les données, il faut synchroniser les threads. Cela va se faire soit par les mutexes, soit en utilisant des variables de conditions.

Les mutexes permettent de bloquer un thread sur une ressource. Un thread acquiers un mutex avec lock() et le relâche avec unlock(). Il est possible d’utiliser un mutex à durée limitée et/ou avec appel réentrant. Pour ne pas lever d’exception à l’acquisition du mutex, on peut utiliser try_lock(). Dans ce cas, tester le résultat pour savoir si l’acquisition est ok. Si des exceptions sont levées par l’utilisation des mutex, il s’agit de system_error. Les codes d’erreur sont récupérés par code().

La gestion des tâches

Le code de gestion des threads reste assez complexe pour réaliser ce qui est finalement de la gestion de tâches. Le développeur est détourné de son objectif initial pour mettre en place toute la cuisine de synchronisation etc. Par ailleurs, il est parfois difficile d’être assuré de la pertinence de créer tant et tant de threads pour rendre le service. La création de threads reste coûteuse. Si vous créez plus de threads que nécessaire, vous perdez en efficacité. Avec la gestion de tâches, nous allons travailler plus haut niveau, le bénéfice est double : mieux se focaliser sur son besoin et laisser les tâches activer ou pas les threads sous-jacents.

Le support des tâches est réalisé à travers quelques classes, telles packaged_task<T>, promise<T>, future<T> ou les fonctions async. Utiliser ces éléments va drastiquement simplifier la mise en oeuvre du parallélisme. Les future et promise représentent respectivement les valeurs à récupérer et la valeur positionnée par la tâche.

Une utilisation simpliste des tâches se réalisent de la façon suivante :

//une_fonction attend un paramètre int et ne retourne rien

std::packaged_task<void(int)> tache1(une_fonction);

std::packaged_task<void(int)> tache2(une_fonction);

//appel de la fonction avec passage de paramètre

tache1(1);

tache2(2);

Les tâches peuvent correspondre aux threads en fonction des possibilités de la machine.

On peut récupérer le résultat d’un calcul lorsqu’il est réalisé seulement (c’est un future ! ). Ici les tâches sont en fait une fonction qui fait la somme entre deux bornes.

std::packaged_task<int(int,int)> tache1(somme_entre);

std::packaged_task<int(int, int)> tache2(somme_entre);

std::future<int> f1(tache1.get_future());

std::future<int> f2(tache2.get_future());

std::thread t1{ std::move(tache1), 1, 51 };

std::thread t2{ std::move(tache2), 51, 100 };

int ret = f1.get() + f2.get();

std::cout << « somme totale «  << ret << std::endl;

L’intérêt de ce genre de code est que le développeur se focalise sur sa tâche, et non sur la mécanique du thread et de la synchronisation. Le « future » est la variable telle que l’appelant l’attend et le correspondant pour la tâche appelée est la « promise ».

Finalement, nous pouvons simplifier ces appels en utilisant std ::async, qui va réduire encore la complexité du code et finalement nous permettre de nous concentrer sur le cœur du problème :

auto future1 = std::async(somme_entre, 1, 51);

auto future2 = std::async(somme_entre, 51, 100);

int res = future1.get() + future2.get();

std::cout << « somme totale «  << res << std::endl;

Conclusion

Pour conclure, le C++11 apporte son lot de nouveautés pour la maîtrise du parallélisme. Nous n’avons pas ici détailler les différentes classes et fonctions offertes par la bibliothèque standard, mais nous avons un aperçu de ce qu’il est possible de faire. Si possible, utiliser la notion de tâche, sinon, se rabattre sur les threads et finalement utiliser les éléments bas niveau. Plus on reste à haut niveau, plus le code est clair et simple.

Pour aller plus loin dans vos projets, consultez notre article sur les fonctions aléatoires en C++11 et leur intégration dans vos programmes.

Facebook
Twitter
LinkedIn
Email
WhatsApp
Par Fabien Brissonneau
Expert en Conception et Développement sur Java et DotNet avec 15 Ans d'Expérience
Fabien, expert en développement, conception et architecture sur les plateformes Java et DotNet, offre ses services depuis plus de 15 ans. Fort de son expérience en C++, il excelle également dans le domaine de la mobilité, ayant créé avec succès des outils pour Android et Windows Phone, certains étant même publiés. En plus de son travail, il consacre une part significative de son temps à la formation, partageant ainsi son expertise avec les acteurs clés de l'industrie. Pour tout ce qui concerne la conception orientée objet sur les plateformes Java et DotNet, Fabien est votre expert de confiance.

Derniers Articles

  • Techniques pour gérer les fichiers texte en C#
  • Créer et lire un fichier CSV avec C#
  • JSON : Comprendre et Utiliser Efficacement
  • Créer une Base SQLite dans C#
  • Lecture des données SQLite simplifiée
Laisser un commentaire Laisser un commentaire

Laisser un commentaire Annuler la réponse

Vous devez vous connecter pour publier un commentaire.

Blog Alphorm
  • Développement
  • 3D et Animation
  • Cybersécurité
  • Infrastructure
  • Virtualisation
  • Réseaux
  • Bureautique
  • BDD
En cours de lecture : La concurrence en C++

© Alphorm - Tous droits réservés