Cours sur les threads en Win32

Développement système natif en c/c++ avec win32 ...

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 46
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Cours sur les threads en Win32

Post by Hydraxx »

Salut, :D

si tu lis ce cours, c’est probablement que tu veux comprendre ce qu’est réellement un thread en Win32, non pas seulement savoir appeler une API, mais comprendre la logique d’exécution sur laquelle repose tout programme Windows un peu sérieux. C’est un point important, parce qu’en pratique beaucoup de débutants manipulent des threads avant meme d’avoir une vision nette de la différence entre un processus et une unité d’exécution. Résultat : le code fonctionne parfois, puis devient instable dès qu’on ajoute un peu de complexité.

Processus et thread : la distinction fondamentale

La première chose à bien fixer, c’est qu’un processus n’exécute rien par lui-meme. Un processus doit etre vu comme un conteneur d’exécution. Il regroupe un espace d’adressage virtuel, des handles vers des objets du noyau, un contexte de sécurité, un heap, des modules chargés, ainsi qu’un ensemble de ressources utiles au programme. Mais pris isolément, le processus reste inerte. Ce n’est pas lui qui avance dans les instructions.

Ce rôle appartient au thread.

Le thread est l’unité d’exécution planifiée par Windows. Lorsqu’un programme démarre, le systeme crée au minimum un thread initial, souvent appelé thread principal, et c’est ce thread qui commence l’exécution de ton code, par exemple dans main() ou WinMain(). Autrement dit, meme si tu n’as jamais créé explicitement de thread, ton programme en possède déjà un.

On peut donc résumer les choses ainsi : le processus contient les ressources, le thread exécute le code. Cette idée parait simple, mais elle est absolument centrale. Beaucoup d’erreurs de conception viennent du fait que l’on parle du “processus qui travaille”, alors qu’en réalité Windows ne planifie jamais un processus pour exécuter des instructions ; il planifie toujours un thread.

Ce qu’est concrètement un thread

D’un point de vue pratique, un thread représente un flux d’exécution indépendant à l’intérieur d’un processus. Il possède son propre contexte processeur, sa propre pile, son propre compteur ordinal d’instruction, et son propre état d’exécution. En revanche, il partage avec les autres threads du meme processus les données communes de ce processus.

Cela signifie qu’un meme processus peut contenir un seul thread, cas le plus simple, ou plusieurs threads actifs simultanément. C’est ce qui permet de découper le travail d’un programme en plusieurs activités : par exemple un thread pour une interface, un autre pour un traitement long, un autre encore pour de l’I/O ou de la communication.

Mais cette puissance a une contrepartie. Puisque les threads d’un meme processus partagent la mémoire, ils peuvent accéder aux memes variables globales, au meme heap, aux memes structures de données. Tant que les accès restent bien ordonnés, tout va bien. Dès que plusieurs threads lisent et modifient une ressource partagée sans coordination, on entre dans le domaine des conditions de concurrence. Et la, les bugs deviennent souvent difficiles à reproduire, donc difficiles à corriger.

Pourquoi plusieurs threads semblent tourner en meme temps

Sur une machine monocoeur, un seul thread s’exécute réellement à un instant donné. Pourtant, comme Windows effectue des changements de contexte très rapides entre les threads prêts à s’exécuter, on a l’impression qu’ils progressent tous ensemble. Cette impression est suffisante pour beaucoup d’applications, mais il faut garder en tete qu’il ne s’agit pas d’un parallélisme matériel réel.

Sur une machine multicœurs, en revanche, plusieurs threads peuvent effectivement s’exécuter en parallèle sur des cœurs distincts. Dans les deux cas, c’est l’ordonnanceur de Windows qui décide quel thread obtient du temps processeur, pendant combien de temps, puis à quel moment il sera interrompu ou repris. Le développeur écrit du code concurrent, mais il ne choisit pas directement l’ordre exact dans lequel le systeme fera tourner chaque thread.

Mémoire partagée et nécessité de la synchronisation

Tous les threads d’un meme processus partagent notamment :
  • l’espace mémoire du processus
  • les variables globales et statiques
  • le heap du processus
  • les objets et structures accessibles par adresse
C’est précisément ce partage qui rend les threads utiles, mais aussi dangereux. Si deux threads accèdent en écriture à une meme donnée sans mécanisme de protection, le résultat peut devenir imprévisible. Dans les cas les plus simples, on observe une valeur corrompue. Dans d’autres cas, on obtient un crash, une incohérence logique, ou un bug intermittent qui n’apparait qu’une fois sur vingt ou cinquante exécutions.

La synchronisation a pour but d’éviter ce type de situation. Elle permet de protéger les ressources partagées, d’imposer un ordre, et de rendre le comportement du programme cohérent meme en présence de plusieurs flux d’exécution. Nous n’entrons pas encore ici dans le détail des mutex, des critical sections ou des évènements, mais il faut déjà retenir qu’un programme multithread sans discipline de synchronisation devient vite fragile.

Création d’un thread avec CreateThread

L’API Win32 la plus directe pour créer un thread est CreateThread. Sa signature est la suivante :

Code: Select all

HANDLE CreateThread(
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    SIZE_T dwStackSize,
    LPTHREAD_START_ROUTINE lpStartAddress,
    LPVOID lpParameter,
    DWORD dwCreationFlags,
    LPDWORD lpThreadId
);
Dans l’idée, le mécanisme est simple : on fournit à Windows l’adresse d’une fonction de départ, éventuellement un paramètre, et le systeme crée un nouveau thread qui commencera son exécution dans cette fonction.

Ce qu’il faut retenir de l’appel est surtout ceci :
  • tu fournis une fonction de départ
  • Windows crée le thread
  • le nouveau thread commence à exécuter cette fonction
  • l’appel retourne un HANDLE vers le thread créé
La fonction exécutée par le thread doit respecter un prototype précis :

Code: Select all

DWORD WINAPI ThreadFunc(LPVOID param);
Le paramètre LPVOID permet de transmettre une donnée arbitraire au thread au moment de sa création. La valeur de retour de la fonction devient, en pratique, le code de sortie du thread.

Premier exemple : exécution dans le thread principal uniquement

Prenons d’abord un exemple volontairement simple, sans thread secondaire :

Code: Select all

#include <Windows.h>
#include <iostream>

int main()
{
    while (true)
    {
        std::cout << "Boucle principale..." << std::endl;
        Sleep(1000);
    }

    return 0;
}
Ici, tout le travail est effectué par le thread principal. La boucle infinie monopolise entièrement ce flux d’exécution. Le programme ne fait donc qu’une seule chose, encore et encore. Tant que ce thread reste occupé dans cette boucle, il ne peut rien faire d’autre. Si l’on imaginait maintenant une interface utilisateur, une attente réseau, ou une opération d’I/O longue, l’ensemble deviendrait rapidement peu réactif.

Le point essentiel n’est pas que la boucle soit infinie, mais qu’il n’existe ici qu’un seul chemin d’exécution. Toute activité supplémentaire devra attendre ou bloquer ce thread unique.

Deuxième exemple : ajout d’un thread secondaire

Voyons maintenant une version avec un thread supplémentaire :

Code: Select all

#include <Windows.h>
#include <iostream>

DWORD WINAPI ThreadFunc(LPVOID param)
{
    while (true)
    {
        std::cout << "Thread secondaire..." << std::endl;
        Sleep(1000);
    }

    return 0;
}

int main()
{
    HANDLE hThread = CreateThread(
        NULL,
        0,
        ThreadFunc,
        NULL,
        0,
        NULL
    );

    while (true)
    {
        std::cout << "Thread principal..." << std::endl;
        Sleep(1000);
    }

    return 0;
}
Dans ce second programme, deux flux d’exécution existent désormais à l’intérieur du meme processus. Le thread principal continue sa propre boucle, tandis que le thread nouvellement créé exécute la fonction ThreadFunc. Les affichages provenant des deux threads peuvent donc s’entrelacer selon la façon dont Windows les planifie.

Ce qu’il faut bien comprendre ici, ce n’est pas uniquement qu’il y a “deux boucles”. Ce qui change réellement, c’est que le programme n’est plus réduit à un seul flux d’exécution. Une tâche peut etre déléguée à un thread pendant qu’une autre continue ailleurs. C’est le début du vrai raisonnement concurrent.

Pourquoi cette séparation peut etre utile

L’intérêt principal d’un thread secondaire apparait dès qu’une opération risque de durer. Une attente réseau, un calcul lourd, une lecture bloquante, une surveillance continue, tout cela peut etre confié à un autre thread pour éviter de paralyser l’exécution principale.

Dans un programme très simple, cette distinction semble parfois artificielle. Mais dès que l’application prend de l’ampleur, séparer les responsabilités devient vite une nécessité.

On peut voir les choses ainsi :
  • sans thread secondaire, une tâche longue immobilise le seul flux d’exécution disponible
  • avec un thread secondaire, ce travail peut etre isolé
  • le thread principal reste alors disponible pour d’autres actions
Evidemment, créer un thread ne résout pas automatiquement tous les problèmes. Cela déplace aussi une partie de la complexité vers la coordination entre threads. Mais utilisé à bon escient, ce mécanisme reste fondamental.

Point important en C et en C++

Un détail souvent négligé par les débutants mérite d’etre signalé tout de suite : CreateThread est une API Win32 brute. En C pur, cela peut convenir dans certains cas simples. En C++, en revanche, il existe un piège classique : CreateThread n’initialise pas le runtime C/C++ de la meme manière que certaines fonctions prévues pour cela.

Concrètement, si le code du thread utilise des éléments dépendant du runtime, par exemple :
  • malloc ou free
  • new ou delete
  • iostream
  • certaines données locales au thread gérées par le runtime
alors l’usage de CreateThread peut mener à des comportements indésirables, allant de fuites mémoire à des instabilités plus discretes. C’est la raison pour laquelle, dans du code C/C++ classique, on préfère souvent _beginthreadex. Ce point fera l’objet d’un cours séparé, parce qu’il mérite une explication à part entière.

Erreur fréquente : vouloir créer trop de threads

Lorsqu’on découvre le multithreading, on peut avoir le reflexe suivant : une tâche égale un thread. Sur le papier, l’idée semble propre. En pratique, elle devient vite mauvaise.

Chaque thread possède un cout de création, un contexte à sauvegarder, une pile, et une charge supplémentaire pour l’ordonnanceur. Multiplier les threads sans nécessité peut dégrader les performances, compliquer le debuggage, et rendre le programme inutilement difficile à maintenir.

Une conception raisonnable repose plutot sur peu de threads, chacun ayant un rôle clair, avec des échanges bien définis et une synchronisation maitrisée. Le multithreading n’est pas un moyen magique d’accélérer n’importe quoi ; c’est un outil d’architecture qu’il faut utiliser avec précision.

Conclusion

Il faut donc retenir que le processus fournit le cadre et les ressources, tandis que le thread constitue la véritable unité d’exécution. Windows planifie les threads, jamais les processus directement. Lorsqu’on appelle CreateThread, on demande au systeme d’ajouter un nouveau flux d’exécution dans le meme espace de processus.

Cela ouvre la porte à des programmes plus réactifs et mieux structurés, mais impose aussi de comprendre les risques liés au partage mémoire et à la concurrence. C’est pour cette raison qu’il vaut mieux bien maitriser les bases avant de multiplier les threads dans tous les sens.

Dans le prochain cours, on pourra justement comparer CreateThread et _beginthreadex, afin de voir pourquoi l’API Win32 brute n’est pas toujours le meilleur choix en C++, et dans quels cas elle peut tout de meme rester utile.

Who is online

Users browsing this forum: No registered users and 0 guests