TD2 – Concurrence entre Threads

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.

Ouvrir avec Coder (depuis Gitlab)

Ouvrir avec Coder (depuis GitHub)

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.

thread_candy_memory

Vous pourrez voir que le nombre de bonbon n’est pas celui qu’il aurait dû être. Voici ce qu’il se passe :

  1. Les threads récupèrent la donnée au début de leur exécution dans une variable locale (privé au thread)
  2. Modifie cette variable locale chacun de leur côté
  3. L’attente de 1 secondes est présente pour être sûr que la concurrence d’accès se reproduise dans tous les cas
  4. 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.

Corriger l’exécution des threads de la routine candy_thread_process() pour qu’il n’y ait pas de concurrence d’accès.

thread_candy_memory_mutex

Lorsque le mutex est en place, l’exécution des deux threads doit être la suivante :

  1. Le premier thread prend le mutex et bloque ainsi le second thread.
  2. Il récupère ensuite la donnée au début de son exécution dans une variable locale (privé au thread).
  3. Modifie la variable locale.
  4. Le premier thread écrit ensuite la variable du nombre de bonbon pendant que le deuxième thread est toujours bloqué.
  5. 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.
Afficher l’état d’un thread qui est dans la boucle sleep et un autre qui est bloqué par le mutex.

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 :

Remplacer le std::mutex par un de ces utilitaires

Vous remarquerez que cela permet :

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 :

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 :

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.

Par Jérémy HERGAULT, le .