Multithreading : comment utiliser les classes de synchronisation
La synchronisation de l'accès aux ressources entre les threads est un problème fréquent dans l'écriture d'applications multithread. Le fait d'avoir deux ou plusieurs threads qui accèdent simultanément aux mêmes données peut conduire à des résultats indésirables et imprévisibles. Par exemple, un thread pourrait mettre à jour le contenu d'une structure pendant qu'un autre thread lirait le contenu de la même structure. Il n'est pas possible de connaître les données que le thread de lecture recevrait : les données anciennes, les données nouvellement écrites ou un possible mélange des deux. MFC fournit un certain nombre de classes de synchronisation et de classes d'accès de synchronisation pour faciliter la résolution de ce problème. Cette rubrique décrit les classes disponibles et explique comment les utiliser pour créer des classes thread-safe dans une application multithread standard.
Une application multithread standard possède une classe qui représente une ressource à partager entre les threads. Avec une classe correctement conçue et complètement thread-safe, il n'est pas nécessaire d'appeler des fonctions de synchronisation. Tout est géré de façon interne dans la classe, ce qui vous permet de vous concentrer sur le meilleur moyen d'utiliser la classe, au lieu d'essayer de déterminer si elle risque d'être altérée. Une technique performante pour créer une classe complètement thread-safe consiste à fusionner la classe de synchronisation dans la classe de ressource. La fusion des classes de synchronisation dans la classe partagée est un processus simple.
À titre d'exemple, choisissez une application qui gère une liste liée de comptes. Cette application permet d'examiner jusqu'à trois comptes dans des fenêtres séparées, mais une seule peut être mise à jour à la fois. Lorsqu'un compte est mis à jour, les données mises à jour sont envoyées via le réseau à une archive de données.
Cet exemple d'application utilise les trois types de classes de synchronisation. Dans la mesure où elle permet d'examiner jusqu'à trois comptes à la fois, elle utilise CSemaphore pour limiter l'accès à trois objets de vue. En cas de tentative d'affichage d'un quatrième compte, l'application attend que l'une des trois premières fenêtres se ferme ou elle échoue. Lorsqu'un compte est mis à jour, l'application utilise CCriticalSection pour s'assurer qu'un seul compte est mis à jour à la fois. Une fois la mise à jour effectuée, elle prévient CEvent, qui libère un thread qui attendait que l'événement soit signalé. Ce thread envoie les nouvelles données à l'archive de données.
Conception d'une classe thread-safe
Pour créer une classe complètement thread-safe, ajoutez d'abord la classe de synchronisation appropriée aux classes partagées en tant que membre de données. Dans le précédent exemple de gestion de comptes, un membre de données CSemaphore serait ajouté à la classe de vue, un membre de données CCriticalSectionserait ajouté à la classe de liste liée et un membre de données CEventserait ajouté à la classe de stockage des données.
Ensuite, ajoutez des appels de synchronisation à toutes les fonctions membres qui modifient les données dans la classe ou accédez à une ressource contrôlée. Dans chaque fonction, vous devez créer un objet CSingleLock ou CMultiLock et appeler la fonction Lock de cet objet. Quand l'objet de verrouillage devient hors de portée et est détruit, le destructeur de l'objet appelle Unlock à votre place, libérant ainsi la ressource. Naturellement, vous pouvez appeler Unlock directement, si vous le souhaitez.
La conception d'une classe thread-safe de cette façon permet de l'utiliser dans une application multithread aussi facilement qu'une classe non thread-safe, mais sans risque. L'encapsulation de l'objet de synchronisation et de l'objet d'accès de synchronisation dans la classe de la ressource offre tous les avantages d'une programmation complètement thread-safe sans l'inconvénient de gestion d'un code de synchronisation.
L'exemple de code suivant illustre cette méthode en utilisant un membre de données, m_CritSection (de type CCriticalSection), déclaré dans la classe de ressources partagées et un objet CSingleLock. Une tentative de synchronisation de la ressource partagée (dérivée de CWinThread) est amorcée par la création d'un objet CSingleLock à l'aide de l'adresse de l'objet m_CritSection. Une tentative est effectuée pour verrouiller la ressource et, une fois cette tâche accomplie, le travail porte sur l'objet partagé. Une fois le travail terminé, la ressource est déverrouillée à l'aide d'un appel à Unlock.
CSingleLock singleLock(&m_CritSection);
singleLock.Lock();
// resource locked
//.usage of shared resource...
singleLock.Unlock();
Notes
Contrairement aux classes de synchronisation MFC, CCriticalSection n'offre pas l'option d'une demande de verrouillage temporisée. Le délai d'attente de la libération d'un thread est illimité.
Cette approche a pour inconvénient que la classe est légèrement plus lente que la même classe sans les objets de synchronisation ajoutés. Par ailleurs, si plusieurs threads risquent de supprimer l'objet, l'approche de fusion ne fonctionne pas toujours. Dans cette situation, il vaut mieux maintenir des objets de synchronisation séparés.
Pour plus d'informations sur la manière de déterminer les classes de synchronisation à utiliser dans les différentes situations, consultez Multithreading : quand utiliser les classes de synchronisation. Pour plus d'informations sur la synchronisation, consultez Synchronization dans le Kit de développement logiciel (SDK) Windows. Pour plus d'informations sur la prise en charge du multithreading dans MFC, consultez Multithreading à l'aide de C++ et de MFC.