Section critique et mutex
Lorsqu'il faut partager des données ou des ressources entre différents thread d'une même application ou de différentes applications, il est impératif de définir quelques règles sous peine d'avoir quelques surprises.
Il faut en effet empêcher (exclure) un thread d'accéder à des variables ou ressources en maj. alors qu'il y a déjà un autre thread qui les utilise. Pour cela, nous allons réaliser des exclusions mutuelles (on pourra lire aussi des mutex pour mutual-exclusion) afin de protéger ces données.
Synchroniser l'accès à des variables entre deux threads dans un même processus:
Imaginez un peu le problème suivant : deux threads qui accèdent à la même partie du code en même temps. Bien entendu, il y a des variables à mettre à jour par exemple des index sur un tableau. Mais comme ils y accèdent en même temps, les résultats risquent fort d'être tout sauf ce que l'on veut. Il faut donc limiter les accès à ce type de varaibles à un seul thread à la fois. Remarque: les variables en question se trouvent dans le heap général du processus. Les variables définies dans les threads n'étant pas accéssibles entre threads.
Une solution consiste à utiliser les sections critiques pour synchroniser tout cela. Les sections critiques sont des mutex simplifiés pour des thread d'un même processus.
Je repète, qu'il s'agit bien d'un même processus. C'est pourquoi les sections critiques ne doivent être utilisées que pour les variables globales d'un processus. On ne pourra pas utiliser cette méthode pour partager des ressources entre différents processus.
Comment ça marche:
Il faut tout d'abord déclarer une variable de type CRITICAL_SECTION généralement globale.
Il faut ensuite initialiser au moins une fois cette section en utilisant la fonction InitializeCriticalSection()
void InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
Avec lpCriticalSection qui est un pointeur sur un objet section critique.
Pour qu'un thread utilise les données, il faudra utiliser la fonction EnterCriticalSection(). Cette fonction va indiquer que l'on veut entrer dans un bloc protégé. Si un thread est déjà entrée, et n'a pas encore fini, la fonction mettra en attente notre nouveau thread.
void EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
On pourra aussi tester la section critique avant d'essayer d'y entrer. Ce qui
peut éviter de bloquer notre thread. On utilisera alors
BOOL TryEnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
La fonction retourne zéro si un thread est déjà entrée
dans la section critique. Sinon, la section critique est vérouillée.
Ne pas oublier de la dévérouiller !
Remarque importante, cette fonction n'est valable qu'à partir de NT4.
Il faut de plus définir ceci pour pouvoir l'utiliser:
#define _WIN32_WINNT 0x0400 au début du code.
Lorsque le thread a terminer, alors il doit indiquer qu'il sort d'un bloc protégé. Pour cela il utilisera la fonction LeaveCriticalSection(). Ce qui autorisera un éventuel thread à utiliser ce bloc protégé.
void LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
Lorsque l'on a fini d'utiliser une section critique, il faudra la supprimer en utilisant la fonction DeleteCriticalSection().
void DeleteCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
Voici un exemple qui va incrémenter un même compteur cpt de 0 à
3 dans 2 threads. Il nous faut synchroniser les threads, sinon le compteur pourrait
devenir fou !
Comme j'affiche des messages dans chacuns des thread, vous pourrez facilement
faire entrer l'un ou l'autre des threads dans la section critique, et ainsi
voir que si un thread est déjà dedans, l'autre ne pourra que dormir
!
Une autre solution devra être utilisée pour le partage de ressource entre différent processus:on sort l'artillerie lourde...
C'est aussi une solution pour limiter le nombre d'application à lancer. Imaginons que vous ne vouliez qu'une seule application à la fois pour une session, il suffira de créer un mutex. Au lancement de notre application, si la fonction retourne que le mutex existe déjà, c'est que notre application est déjà lancée.
On va créer des mutex. Pour pouvoir retrouver un mutex entre différentes applications, l'identifiant d'un mutex est un nom. Il suffira donc aux applications (voir processus) d'utiliser le même nom entre-elles(eux).
CreateMutex() va nous permettre de créer un mutex ou de l'ouvrir s'il existe déjà.
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName
);
Les paramètres sont:
lpMutexAttributes : pointeur sur une structure de type SECURITY_ATTRIBUTES pour déterminer si le handle du mutex pourra être hérité ou non par des processus enfants. Si NULL, pas d'héritage possible.
bInitialOwner : TRUE pour indiquer que l'on veut être le propriétaire
initial. Le mutex ne sera alors pas signalé à un autre thread
tant que le thread ayant créé le mutex ne l'aura pas libéré.
Si la valeur est FALSE, c'est le système qui décidera qui pourra
utiliser le mutex. les autres ne pourront qu'attendre leur tour.
lpName : Nom du mutex.
Attention: ne pas donner un nom de mutex déjà utilisé par une autre application n'ayant rien à voir avec la notre. De plus, le test effectuer par windows sur le nom est case sensitif.
C'est une chaîne de caractères qui doit se terminer par NULL. Evidemment, on peut mettre NULL comme nom, dans ce cas, le mutex n'aura pas de nom...
On peut préfixer le nom par Global\ ou local\ pour indiquer que l'objet
est global ou uniquement dans une session (jamais essayé...)
Si le nom est identique à un objet évenement, semaphore, waitable timer, job, ou file-mapping, , la création du mutex echoue et GetLastError retourne le code ERROR_INVALID_HANDLE.
Valeur retournée:
Si la fonction n'échoue pas, la valeur retournée est un handle sur un mutex. Mais il faudra encore tester si nous l'avons créé ou simplement ouvert. Pour cela, il est impératif de tester GetLastError. Si la valeur est ERROR_ALREADY_EXISTS, alors nous ne l'avons pas créé...il y a déjà quelqu'un !
Si la valeur est NULL, c'est qu'il y a un problème.
Remarque:
Un mutex ne peut être utilisé que par un seul thread à la fois. les autres doivent attendre.
Un objet de type mutex est dit signalé si aucun thread ne s'est approprié le dit mutex sinon il est dans un état non signalé. Un thread peut s'approprié un mutex immédiatement après la création du mutex grâce au flag bInitialOwner à TRUE. Sinon, il faudra utiliser les fonctions d'attentes (ces fonctions sont vues un peu plus loin: WaitForSingleObject par exemple). Ces fonctions d'attentes vont mettre en sommeil le thread, jusqu'à ce qu'il soit autorisé à s'approprier le mutex.
Pour mettre dans un état signalé un mutex, il faut utiliser ReleaseMutex. (A noter qu'un mutex se libèrera quand même lors de la mort d'un thread (évidemment si ce thread c'était approprié le mutex). On peut aussi mettre fin à l'utilisation d'un mutex par CloseHandle (jamais essayé).
Un mutex sera détruit lorsqu'il n'y aura plus de handle pour ce mutex. En clair, lorsqu'il n'y aura plus de thread ayant demandé une création sur ce mutex.
Enfin, un thread qui voudrait attendre un état signalé d'un mutex alors qu'il c'est déjà approprié le mutex ne s'arrêterai pas sur cette demande et continuerai son traitement, ceci afin d'éviter les deadlock.
La fonction OpenMutex() permet d'ouvrir un mutex.
Le seul intérêt pour moi de cette fontion est de vérifier qu'une application est bien lancée. Genre un serveur de base de donnée:si le serveur n'est pas lancé, alors inutile de continuer car la base ne sera pas accéssible.
Evidemment, il faudra qu'avant une application ait fait un CreateMutex...
HANDLE OpenMutex(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName
);
Les paramètres sont identiques à CreateMutex.
La nouveauté vient du champ bInheritHandle qui si à TRUE va permettre l'héritage du handle lors d'un CreateProcess.
Valeur retournée:
NULL si problème. Par exemple mutex inexistant sera retourné par GetLastError avec le code ERROR_FILE_NOT_FOUND.
Sinon, c'est le handle du mutex.
Enfin, ReleaseMutex() pour changer l'affectation du mutex:
BOOL ReleaseMutex(
HANDLE hMutex
);
Remarque: Evidemment, un thread ne peut pas devenir propriétaire du mutex si un autre thread est déjà propriétaire. Dans ce cas, la fonction va échouer...
Pour clôturer le mutex, il faut utiliser CloseHandle(hdle);
Voici un exemple d'utilisation de mutex sur une ressource (bon ici, cette ressource est fantome):
Ou encore, s'assurer qu'il n'y a bien qu'une instance de notre application: