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. 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);
Table de matière
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().