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
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
);
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éé
Code: Select all
DWORD WINAPI ThreadFunc(LPVOID param);
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;
}
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;
}
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
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
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.
