Multithreading à l'aide de C et de Win32

Le compilateur Microsoft C/C++ (MSVC) prend en charge la création d’applications multithread. Envisagez d’utiliser plusieurs threads si votre application doit effectuer des opérations coûteuses qui entraîneraient l’absence de réponse de l’interface utilisateur.

Avec MSVC, il existe plusieurs façons de programmer avec plusieurs threads : vous pouvez utiliser C++/WinRT et la bibliothèque Windows Runtime, la bibliothèque Microsoft Foundation Class (MFC), C++/CLI et le runtime .NET, ou la bibliothèque d’exécution C et l’API Win32. Cet article traite de la multithreading en C. Pour obtenir un exemple de code, consultez l’exemple de programme multithread en C.

Programmes multithreads

Un thread est essentiellement un chemin d’exécution via un programme. Il s’agit également de la plus petite unité d’exécution planifiée par Win32. Un thread se compose d’une pile, de l’état des registres du processeur et d’une entrée dans la liste d’exécution du planificateur système. Chaque thread partage toutes les ressources du processus.

Un processus se compose d’un ou plusieurs threads et du code, des données et d’autres ressources d’un programme en mémoire. Les ressources de programme standard sont des fichiers ouverts, des sémaphores et une mémoire allouée dynamiquement. Un programme s’exécute lorsque le planificateur système fournit l’un de ses contrôles d’exécution de threads. Le planificateur détermine quels threads doivent s’exécuter et quand ils doivent s’exécuter. Les threads de priorité inférieure peuvent être obligés d’attendre pendant que les threads de priorité supérieure terminent leurs tâches. Sur les machines multiprocesseurs, le planificateur peut déplacer des threads individuels vers différents processeurs pour équilibrer la charge du processeur.

Chaque thread d’un processus fonctionne indépendamment. Sauf si vous les rendez visibles les uns aux autres, les threads s’exécutent individuellement et ne connaissent pas les autres threads d’un processus. Toutefois, les threads partageant des ressources courantes doivent coordonner leur travail à l’aide de sémaphores ou d’une autre méthode de communication interprocesseur. Pour plus d’informations sur la synchronisation des threads, consultez Écriture d’un programme Win32 multithread.

Prise en charge des bibliothèques pour le multithreading

Toutes les versions du CRT prennent désormais en charge la multithreading, à l’exception des versions non verrouillées de certaines fonctions. Pour plus d’informations, consultez les performances des bibliothèques multithread. Pour plus d’informations sur les versions du CRT disponibles pour établir un lien avec votre code, consultez les fonctionnalités de la bibliothèque CRT.

Fichiers include pour le multithreading

Les fichiers CRT standard déclarent les fonctions de bibliothèque runtime C à mesure qu’elles sont implémentées dans les bibliothèques. Si vos options de compilateur spécifient les conventions d’appel __fastcall ou __vectorcall , le compilateur suppose que toutes les fonctions doivent être appelées à l’aide de la convention d’appel d’inscription. Les fonctions de bibliothèque d’exécution utilisent la convention d’appel C et les déclarations dans les fichiers include standard indiquent au compilateur de générer des références externes correctes à ces fonctions.

Fonctions CRT pour le contrôle de thread

Tous les programmes Win32 ont au moins un thread. N’importe quel thread peut créer des threads supplémentaires. Un thread peut terminer son travail rapidement, puis se terminer, ou il peut rester actif pour la vie du programme.

Les bibliothèques CRT fournissent les fonctions suivantes pour la création et l’arrêt des threads : _beginthread, _beginthreadex, _endthread et _endthreadex.

Les _beginthread fonctions et _beginthreadex créent un thread et retournent un identificateur de thread si l’opération réussit. Le thread se termine automatiquement s’il termine l’exécution. Ou bien, il peut se terminer par un appel à _endthread ou _endthreadex.

Remarque

Si vous appelez des routines d’exécution C à partir d’un programme créé avec libcmt.lib, vous devez démarrer vos threads avec la ou _beginthreadex la _beginthread fonction. N’utilisez pas les fonctions ExitThread Win32 et CreateThread. L’utilisation SuspendThread peut entraîner un blocage lorsque plusieurs threads sont bloqués en attendant que le thread suspendu termine son accès à une structure de données runtime C.

Fonctions _beginthread et _beginthreadex

Les _beginthread fonctions et _beginthreadex les fonctions créent un thread. Un thread partage le code et les segments de données d’un processus avec d’autres threads du processus, mais possède ses propres valeurs d’inscription uniques, espace de pile et adresse d’instruction actuelle. Le système donne du temps processeur à chaque thread, afin que tous les threads d’un processus puissent s’exécuter simultanément.

_beginthread et _beginthreadex sont similaires à la fonction CreateThread dans l’API Win32, mais présentent ces différences :

  • Ils initialisent certaines variables de bibliothèque d’exécution C. Cela n’est important que si vous utilisez la bibliothèque d’exécution C dans vos threads.

  • CreateThread permet de contrôler les attributs de sécurité. Vous pouvez utiliser cette fonction pour démarrer un thread dans un état suspendu.

_beginthread et _beginthreadex retournez un handle au nouveau thread en cas de réussite ou de code d’erreur en cas d’erreur.

Fonctions _endthread et _endthreadex

La fonction _endthread met fin à un thread créé par _beginthread (et de la même façon, _endthreadex met fin à un thread créé par _beginthreadex). Les threads se terminent automatiquement lorsqu’ils se terminent. _endthread et _endthreadex sont utiles pour l’arrêt conditionnel à partir d’un thread. Un thread dédié au traitement des communications, par exemple, peut cesser s’il n’est pas en mesure d’obtenir le contrôle du port de communication.

Écriture d’un programme Win32 multithread

Lorsque vous écrivez un programme avec plusieurs threads, vous devez coordonner leur comportement et l’utilisation des ressources du programme. Vérifiez également que chaque thread reçoit sa propre pile.

Partage de ressources courantes entre threads

Remarque

Pour une discussion similaire du point de vue MFC, consultez Multithreading : Programmation Astuces et Multithreading : Quand utiliser les classes de synchronisation.

Chaque thread a sa propre pile et sa propre copie des registres d’UC. D’autres ressources, telles que les fichiers, les données statiques et la mémoire du tas, sont partagées par tous les threads du processus. Les threads utilisant ces ressources courantes doivent être synchronisés. Win32 offre plusieurs façons de synchroniser des ressources, notamment des sémaphores, des sections critiques, des événements et des mutex.

Lorsque plusieurs threads accèdent à des données statiques, votre programme doit fournir des conflits de ressources possibles. Envisagez un programme dans lequel un thread met à jour une structure de données statiques contenant des coordonnées x,y pour que les éléments soient affichés par un autre thread. Si le thread de mise à jour modifie la coordonnée x et est préempté avant de pouvoir modifier la coordonnée y , le thread d’affichage peut être planifié avant la mise à jour de la coordonnée y . L’élément s’affiche à l’emplacement incorrect. Vous pouvez éviter ce problème en utilisant des sémaphores pour contrôler l’accès à la structure.

Un mutex (court pour l’exclusionmutuelle) est un moyen de communiquer entre les threads ou les processus qui s’exécutent de manière asynchrone les uns des autres. Cette communication peut être utilisée pour coordonner les activités de plusieurs threads ou processus, généralement en contrôlant l’accès à une ressource partagée en verrouillant et déverrouillent la ressource. Pour résoudre ce problème de mise à jour de coordonnées x,y, le thread de mise à jour définit un mutex indiquant que la structure de données est en cours d’utilisation avant d’effectuer la mise à jour. Il effacerait le mutex une fois les deux coordonnées traitées. Le thread d’affichage doit attendre que le mutex soit effacé avant de mettre à jour l’affichage. Ce processus d’attente d’un mutex est souvent appelé blocage sur un mutex, car le processus est bloqué et ne peut pas continuer tant que le mutex n’est pas effacé.

Le programme Bounce.c présenté dans l’exemple de programme C multithread utilise un mutex nommé ScreenMutex pour coordonner les mises à jour de l’écran. Chaque fois qu’un des threads d’affichage est prêt à écrire à l’écran, il appelle WaitForSingleObject avec le handle vers ScreenMutex et la constante INFINITE pour indiquer que l’appel WaitForSingleObject doit bloquer sur le mutex et ne pas expirer. Si ScreenMutex elle est claire, la fonction d’attente définit le mutex afin que d’autres threads ne puissent pas interférer avec l’affichage et continuent à exécuter le thread. Sinon, le thread se bloque jusqu’à ce que le mutex s’efface. Lorsque le thread termine la mise à jour d’affichage, il libère le mutex en appelant ReleaseMutex.

Les écrans d’affichage et les données statiques ne sont que deux des ressources nécessitant une gestion minutieuse. Par exemple, votre programme peut avoir plusieurs threads accédant au même fichier. Étant donné qu’un autre thread peut avoir déplacé le pointeur de fichier, chaque thread doit réinitialiser le pointeur de fichier avant de lire ou d’écrire. En outre, chaque thread doit s’assurer qu’il n’est pas préempté entre le moment où il positionne le pointeur et l’heure à laquelle il accède au fichier. Ces threads doivent utiliser un sémaphore pour coordonner l’accès au fichier en crochetant chaque accès au fichier avec WaitForSingleObject et ReleaseMutex aux appels. L’exemple de code suivant illustre cette technique :

HANDLE    hIOMutex = CreateMutex (NULL, FALSE, NULL);

WaitForSingleObject( hIOMutex, INFINITE );
fseek( fp, desired_position, 0L );
fwrite( data, sizeof( data ), 1, fp );
ReleaseMutex( hIOMutex);

Piles de threads

L’espace de pile par défaut d’une application est alloué au premier thread d’exécution, appelé thread 1. Par conséquent, vous devez spécifier la quantité de mémoire à allouer pour une pile distincte pour chaque thread supplémentaire dont votre programme a besoin. Le système d’exploitation alloue un espace de pile supplémentaire pour le thread, si nécessaire, mais vous devez spécifier une valeur par défaut.

Le premier argument de l’appel _beginthread est un pointeur vers la BounceProc fonction, qui exécute les threads. Le deuxième argument spécifie la taille de pile par défaut pour le thread. Le dernier argument est un numéro d’ID passé à BounceProc. BounceProc utilise le numéro d’ID pour amorçage du générateur de nombres aléatoires et pour sélectionner l’attribut de couleur et le caractère d’affichage du thread.

Les threads qui effectuent des appels à la bibliothèque d’exécution C ou à l’API Win32 doivent autoriser suffisamment d’espace de pile pour les fonctions bibliothèque et API qu’ils appellent. La fonction C printf nécessite plus de 500 octets d’espace de pile et vous devez disposer de 2 000 octets d’espace de pile disponibles lors de l’appel de routines d’API Win32.

Étant donné que chaque thread a sa propre pile, vous pouvez éviter les collisions potentielles sur les éléments de données à l’aide de données aussi peu statiques que possible. Concevez votre programme pour utiliser des variables de pile automatiques pour toutes les données qui peuvent être privées à un thread. Les seules variables globales du programme Bounce.c sont des mutex ou des variables qui ne changent jamais après leur initialisation.

Win32 fournit également un stockage tls (Thread-Local Storage) pour stocker des données par thread. Pour plus d’informations, consultez Le stockage local thread (TLS).

Éviter les sources de problèmes dans les programmes multithreads

Il existe plusieurs problèmes que vous pouvez rencontrer lors de la création, de la liaison ou de l’exécution d’un programme C multithread. Certains des problèmes les plus courants sont décrits dans le tableau suivant. (Pour une discussion similaire du point de vue MFC, consultez Multithreading : programmation Astuces.)

Problème Cause probable
Vous obtenez une boîte de message indiquant que votre programme a provoqué une violation de protection. De nombreuses erreurs de programmation Win32 provoquent des violations de protection. Une cause courante de violations de protection est l’affectation indirecte de données à des pointeurs Null. Étant donné que votre programme tente d’accéder à la mémoire qui ne lui appartient pas, une violation de protection est émise.

Un moyen simple de détecter la cause d’une violation de protection consiste à compiler votre programme avec des informations de débogage, puis à l’exécuter via le débogueur dans l’environnement Visual Studio. Lorsque l’erreur de protection se produit, Windows transfère le contrôle au débogueur et le curseur est positionné sur la ligne qui a provoqué le problème.
Votre programme génère de nombreuses erreurs de compilation et de liaison. Vous pouvez éliminer de nombreux problèmes potentiels en définissant le niveau d’avertissement du compilateur sur l’une de ses valeurs les plus élevées et en hetant les messages d’avertissement. En utilisant les options de niveau 3 ou 4 d’avertissement de niveau 4, vous pouvez détecter les conversions de données involontaires, les prototypes de fonction manquants et l’utilisation des fonctionnalités non ANSI.

Voir aussi

Prise en charge du multithreading pour le code plus ancien (Visual C++)
Exemple de programme multithread en C
Stockage local de thread (TLS)
Opérations concurrentes et asynchrones avec C++/WinRT
Multithreading à l’aide de C++ et de MFC