Ce TD fait suite au TD1 pour mettre en lumière les problèmes de concurrences d’accès qui peuvent apparaissent lors de l’implémentation des threads.
La template pour réaliser ce TD est la même que pour le TD1. Les fonctions que vous manipulerez pour ce TD se trouvent toutes dans l’objet ThreadConcurrency
.
Pour toutes les actions suivantes, vous pouvez utiliser la librairie de votre choix : Pthread ou std::thread. Néanmoins la correction sera donnée pour la librairie Pthread.
Mutexes
Pour montrer le problème de concurrence d’accès entre threads, poussons plus loin le partage de variables. Pour cela plusieurs routines de threads sont disponibles.
Tout d’abord, lancer 2 (ou plus) threads qui exécute la routine
candy_thread_process()
, et afficher le nombre de bonbons après que les threads se soient exécutés.
Vous pourrez voir que le nombre de bonbon n’est pas celui qu’il aurait dû être. Voici ce qu’il se passe :
- Les threads récupèrent la donnée au début de leur exécution dans une variable locale (privé au thread)
- Modifie cette variable locale chacun de leur côté
- L’attente de 1 secondes est présente pour être sûr que la concurrence d’accès se reproduise dans tous les cas
- Les threads écrivent chacun la variable du nombre de bonbon (ils écrasent la valeur les un à la suite des autres)
C’est bien à l’écriture de chaque thread que le problème apparait. Vu que les threads ne s’attendent pas mutuellement, ils écrivent une donnée sans prendre en compte les traitements que les autres ont pu faire.
C’est là qu’interviennent les mutexes. Ils vont permettent d’attendre qu’un thread ait fini son traitement pour qu’un autre thread puisse faire le sien.
En termes de librairie, là encore plusieurs choix s’offrent à vous pour mettre en place un mutex.
Pthread_mutex
La librairie Pthread fournit un ensemble d’éléments pour la synchronisation des threads.
Elle fournit entre autres un outil de gestion des mutexes.
La structure de donnée d’un mutex Pthread est pthread_mutex_t
.
pthread_mutex_lock()
pthread_mutex_unlock()
Corriger l’exécution des threads de la routine
candy_thread_process()
pour qu’il n’y ait pas de concurrence d’accès.
Lorsque le mutex est en place, l’exécution des deux threads doit être la suivante :
- Le premier thread prend le mutex et bloque ainsi le second thread.
- Il récupère ensuite la donnée au début de son exécution dans une variable locale (privé au thread).
- Modifie la variable locale.
- Le premier thread écrit ensuite la variable du nombre de bonbon pendant que le deuxième thread est toujours bloqué.
- Il libère le mutex permettant au second thread de faire exactement la même exécution, à la différence que le second thread copie la donnée modifiée au début de son exécution.
std::mutex
Comme pour les threads de la standard librairy, les mutexes ont été implémentés depuis le C++11. Ils sont équivalents aux threads Pthread.
Remplacer le mutex Pthread mis en place précédemment par un std::mutex.
Utilitaire std::mutex
Un des avantages de la librairie std::thread est qu’elle fournit des manipulateurs pour ses mutexes :
std::lock_guard<std::mutex>
: Permet de verrouiller le mutex jusqu’à destruction de l’objet.std::unique_lock<std::mutex>
: Similaire àlock_guard
, mais permet en plus de pouvoir lock/unlock pendant la durée de vie de l’objet.std::timed_mutex
: Permet d’attendre de pouvoir verrouiller jusqu’à un certain temps, ou de verrouiller un mutex un temps donnée.
Remplacer le std::mutex par un de ces utilitaires
Vous remarquerez que cela permet :
- de simplifier le code (une seule ligne est nécessaire au lieu de deux)
- d’éviter la duplication de code des lock/unlock si le code se split
- lors du return, cela évite de passer par un objet temporaire le temps de déverrouiller la ressource
std::atomic
Une autre manière de synchroniser une variable est d’utiliser un std::atomic. Ce mécanisme permet de garantir que la lecture/écriture de cette variable sera thread-safe.
Utilisez un
std::atomic_int
pour synchroniser la variable candy à la place du mutex
Lorsque vous utiliserez l’objet atomique, vous devrez spécifier l’ordre dans lequel la variable sera accédée via std::memory_order. La documentation vous montre plusieurs exemples de l’utilisation de cet ordre mémoire.
Lecteurs/Rédacteurs
Ce modèle est utilisé lorsque des threads veulent pouvoir accéder à une donnée en lecture uniquement alors que d’autres veulent y accéder en écriture. En effet le problème de concurrence sur une donnée ne se présente que quand la variable est écrite par un autre thread. Le but est donc de pouvoir avoir :
- N lecteurs et aucun rédacteur. Cela permet aux readers de lire la donnée tous en même temps. Cela ne provoque pas de concurrence d’accès vu que la donnée n’est jamais modifiée.
- 0 lecteur et 1 rédacteur. Les readeurs ne peuvent plus lire la donnée et laisse donc 1 rédacteur la modifier. Au contraire des lecteurs, il ne peut y avoir qu’un rédacteur à la fois qui modifie la donnée.
Pour l’implémentation de ce modèle, la std fournie la class std::shared_mutex. Le problème est que cette implémentation n’est disponible qu’en C++17. Et vu que gcc ne l’implémente que dans ses dernières versions, nous allons préférer utiliser les objets de la librairie Pthread.
La librairie Pthread fourni les fonctions :
pthread_rwlock_t
: Verrou spécifique lecteurs/rédacteurspthread_rwlock_init()
: Initialise le verroupthread_rwlock_rdlock()
: Verrouille la ressource pour lirepthread_rwlock_wrlock()
: Verrouille la ressource pour écrirepthread_rwlock_unlock()
: Déverrouille la ressource (lecture ou écriture)pthread_rwlock_destroy()
: Détruit le verrou partagé
Implémenter un rédacteur qui modifie le nombre de candy, et plusieurs lecteurs qui affichent le nombre de candy
Le principal problème de ce modèle est que s’il y a toujours au moins un lecteur sur la donnée, les rédacteurs ne peuvent jamais modifier la donnée et un phénomène de famine ce produit. Avec la donnée qui n’est jamais modifiée, votre programme peut aussi potentiellement se bloquer.
Producteurs/Consommateurs
Le modèle producteur/consommateur est sans doute celui le plus utilisé. Beaucoup d’applications utilisent ce modèle pour l’échange de message sans pertes ou pour simplifier l’échange interne ou externe. Les brokers les plus connus sont :
Depuis le C++11, la std fourni un objet utile pour la synchronisation de un ou plusieurs threads std::condition_variable
Mettre en place une usine de bonbon. Plusieurs threads devront produire des candy et d’autres les consommer. Le nombre de bonbon devra toujours être positif.