Classe de thread dans Delphi
Raptor[Studio Mental]
http://mental.mentsu.com
Quatre
Critical Section (CriticalSection) est une technologie de protection de l'accès aux données partagées. C'est en fait équivalent à une variable booléenne globale. Mais son fonctionnement est différent. Il n'a que deux opérations : Enter et Leave. Ses deux états peuvent également être considérés comme True et False, indiquant respectivement s'il se trouve dans la section critique. Ces deux opérations sont également des primitives, elles peuvent donc être utilisées pour protéger les données partagées contre les violations d'accès dans les applications multithread.
La méthode d'utilisation des sections critiques pour protéger les données partagées est très simple : appelez Enter pour définir l'indicateur de section critique avant chaque accès aux données partagées, puis exploitez les données, et enfin appelez Leave pour quitter la section critique. Son principe de protection est le suivant : après qu'un thread entre dans la section critique, si un autre thread veut également accéder aux données à ce moment-là, il constatera qu'un thread est déjà entré dans la section critique lors de l'appel d'Entrée, et alors ce thread sera Raccrochez et attendez que le thread actuellement dans la section critique appelle Leave pour quitter la section critique. Lorsqu'un autre thread termine l'opération et appelle Leave pour quitter, ce thread sera réveillé, définira l'indicateur de section critique et commencera à exploiter les données. empêchant ainsi les conflits d’accès.
En prenant le précédent InterlockedIncrement comme exemple, nous utilisons CriticalSection (API Windows) pour l'implémenter :
Var
InterlockedCrit : TRTLCriticalSection ;
PRocedure InterlockedIncrement( var aValue : Integer );
Commencer
EnterCriticalSection (InterlockedCrit);
Inc(uneValeur);
LeaveCriticalSection(InterlockedCrit);
Fin;
Regardez maintenant l'exemple précédent :
1. Le fil A entre dans la section critique (en supposant que les données sont 3)
2. Le thread B entre dans la section critique Comme A est déjà dans la section critique, B est suspendu.
3. Le fil A en ajoute un aux données (maintenant 4)
4. Le thread A quitte la section critique et réveille le thread B (les données actuelles dans la mémoire sont 4)
5. Le thread B se réveille et en ajoute un aux données (il est maintenant 5)
6. Le fil B quitte la section critique et les données actuelles sont correctes.
C'est ainsi que les sections critiques protègent l'accès aux données partagées.
Concernant l’utilisation des sections critiques, une chose à noter est la gestion des exceptions lors de l’accès aux données. Parce que si une exception se produit pendant l'opération de données, l'opération Leave ne sera pas exécutée. Par conséquent, le thread qui devrait être réveillé ne sera pas réveillé, ce qui peut empêcher le programme de répondre. De manière générale, la bonne approche consiste donc à utiliser les sections critiques comme suit :
EnterCriticalSection
Essayer
//Exploiter les données de section critiques
Enfin
Quitter la section critique
Fin;
La dernière chose à noter est qu'Event et CriticalSection sont tous deux des ressources du système d'exploitation, qui doivent être créées avant utilisation et publiées après utilisation. Par exemple, un événement global : SyncEvent et un CriticalSection : TheadLock global utilisés par la classe TThread sont tous deux créés et publiés dans InitThreadSynchronization et DoneThreadSynchronization, et ils sont appelés dans l'unité Initialisation et Finalisation de l'unité Classes.
Puisque les API sont utilisées pour faire fonctionner Event et CriticalSection dans TThread, l'API est utilisée comme exemple ci-dessus. En fait, Delphi en a fourni l'encapsulation dans l'unité SyncObjs, il s'agit respectivement de la classe TEvent et de la classe TCriticalSection. L'utilisation est presque la même que la méthode précédente d'utilisation de l'API. Le constructeur de TEvent ayant trop de paramètres, pour plus de simplicité, Delphi fournit également une classe Event initialisée avec des paramètres par défaut : TSimpleEvent.
Au fait, permettez-moi de vous présenter une autre classe utilisée pour la synchronisation des threads : TMultiReadExclusiveWriteSynchronizer, qui est définie dans l'unité SysUtils. Pour autant que je sache, il s'agit du nom de classe le plus long défini dans Delphi RTL. Heureusement, il a un alias court : TMREWSync. Quant à son utilisation, je pense qu’on peut le connaître rien qu’en regardant le nom, donc je n’en dirai pas plus.
Avec les connaissances préparatoires précédentes sur Event et CriticalSection, nous pouvons officiellement commencer à discuter de Synchronize et WaitFor.
Nous savons que Synchronize réalise la synchronisation des threads en plaçant une partie du code dans le thread principal pour exécution, car dans un processus, il n'y a qu'un seul thread principal. Examinons d'abord l'implémentation de Synchronize :
procédure TThread.Synchronize(Méthode : TThreadMethod);
commencer
FSynchronize.FThread := Soi ;
FSynchronize.FSynchronizeException := nul;
FSynchronize.FMethod := Méthode ;
Synchroniser (@FSynchronize);
fin;
où FSynchronize est un type d'enregistrement :
PSynchronizeRecord = ^TSynchronizeRecord;
TSynchronizeRecord = enregistrement
FThread : TObject ;
Méthode F : TThreadMethod ;
FSynchronizeException : TObject ;
fin;
Utilisé pour l'échange de données entre les threads et le thread principal, y compris les objets de classe de thread entrants, les méthodes de synchronisation et les exceptions qui se produisent.
Une version surchargée de celui-ci est appelée dans Synchronize, et cette version surchargée est assez particulière, c'est une "méthode de classe". La méthode dite de classe est une méthode membre de classe spéciale. Son invocation ne nécessite pas la création d'une instance de classe, mais est appelée via le nom de la classe comme un constructeur. La raison pour laquelle il est implémenté à l'aide d'une méthode de classe est qu'il peut être appelé même lorsque l'objet thread n'est pas créé. Cependant, dans la pratique, une autre version surchargée de celui-ci (également une méthode de classe) et une autre méthode de classe StaticSynchronize sont utilisées. Voici le code de cette synchronisation :
procédure de classe TThread.Synchronize(ASyncRec: PSynchronizeRecord);
var
SyncProc : TSyncProc ;
commencer
si GetCurrentThreadID = MainThreadID alors
Méthode ASyncRec.F
autre
commencer
SyncProc.Signal := CreateEvent(nil, True, False, néant);
essayer
EnterCriticalSection(ThreadLock);
essayer
si SyncList = nul alors
SyncList := TList.Create;
SyncProc.SyncRec := ASyncRec;
SyncList.Add (@SyncProc);
SignalSyncEvent ;
si assigné (WakeMainThread) alors
WakeMainThread(SyncProc.SyncRec.FThread);
LeaveCriticalSection(ThreadLock);
essayer
WaitForSingleObject(SyncProc.Signal, INFINITE);
enfin
EnterCriticalSection(ThreadLock);
fin;
enfin
LeaveCriticalSection(ThreadLock);
fin;
enfin
CloseHandle(SyncProc.Signal);
fin;
si assigné (ASyncRec.FSynchronizeException), alors déclenchez ASyncRec.FSynchronizeException ;
fin;
fin;
Ce code est un peu plus long, mais ce n'est pas trop compliqué.
La première consiste à déterminer si le thread actuel est le thread principal. Si tel est le cas, exécutez simplement la méthode de synchronisation et revenez.
S'il ne s'agit pas du thread principal, il est prêt à démarrer le processus de synchronisation.
Les données d'échange de thread (paramètres) et un handle d'événement sont enregistrés via la variable locale SyncProc. La structure d'enregistrement est la suivante :
TSyncProc=enregistrement
SyncRec : PSynchronizeRecord ;
Signal : TPoignée ;
fin;
Créez ensuite un événement, puis entrez dans la section critique (via la variable globale ThreadLock, car un seul thread peut entrer dans l'état Synchroniser en même temps, vous pouvez donc utiliser la variable globale pour enregistrer), puis stockez les données enregistrées dans le Liste SyncList (si cette liste n'existe pas, créez-la). On peut voir que la section critique de ThreadLock est de protéger l'accès à SyncList. Cela sera revu lorsque CheckSynchronize sera introduit ultérieurement.
L'étape suivante consiste à appeler SignalSyncEvent. Son code a déjà été introduit lors de l'introduction du constructeur TThread. Sa fonction est simplement d'effectuer une opération Set sur SyncEvent. Le but de ce SyncEvent sera détaillé plus tard lorsque WaitFor sera introduit.
Vient ensuite la partie la plus importante : l’appel de l’événement WakeMainThread pour les opérations de synchronisation. WakeMainThread est un événement global de type TNotifyEvent. La raison pour laquelle les événements sont utilisés pour le traitement ici est que la méthode Synchronize place essentiellement le processus qui doit être synchronisé dans le thread principal pour être exécuté via des messages. Elle ne peut pas être utilisée dans certaines applications sans boucles de messages (telles que la console ou la DLL). , utilisez donc cet événement pour le traitement.
L'objet application répond à cet événement. Les deux méthodes suivantes sont utilisées pour définir et effacer la réponse à l'événement WakeMainThread (à partir de l'unité Forms) :
procédure TApplication.HookSynchronizeWakeup ;
commencer
Classes.WakeMainThread := WakeMainThread;
fin;
procédure TApplication.UnhookSynchronizeWakeup ;
commencer
Classes.WakeMainThread := néant;
fin;
Les deux méthodes ci-dessus sont appelées respectivement dans le constructeur et le destructeur de la classe TApplication.
Il s'agit du code qui répond à l'événement WakeMainThread dans l'objet Application. Le message est envoyé ici. Il utilise un message vide pour y parvenir :
procédure TApplication.WakeMainThread(Sender: TObject);
commencer
PostMessage (Poignée, WM_NULL, 0, 0);
fin;
La réponse à ce message se trouve également dans l'objet Application, voir le code suivant (supprimez les parties non pertinentes) :
procédure TApplication.WndProc(var Message : TMessage);
…
commencer
essayer
…
avec Message faire
cas Msg de
…
WM_NULL :
VérifierSynchroniser ;
…
sauf
HandleException(Soi);
fin;
fin;
Parmi eux, CheckSynchronize est également défini dans l'unité Classes. Comme il est relativement complexe, nous ne l'expliquerons pas en détail pour le moment. Sachez simplement que c'est la partie qui gère spécifiquement la fonction Synchronize. code.
Après avoir exécuté l'événement WakeMainThread, quittez la section critique, puis appelez WaitForSingleObject pour commencer à attendre l'événement créé avant d'entrer dans la section critique. La fonction de cet événement est d'attendre la fin de l'exécution de cette méthode de synchronisation. Cela sera expliqué plus tard lors de l'analyse de CheckSynchronize.
Notez qu'après WaitForSingleObject, vous entrez à nouveau dans la section critique, mais sortez sans rien faire. Cela semble inutile, mais c'est nécessaire !
Parce que Enter et Leave dans la section critique doivent correspondre strictement à un-à-un. Alors peut-on le changer comme ceci :
si assigné (WakeMainThread) alors
WakeMainThread(SyncProc.SyncRec.FThread);
WaitForSingleObject(SyncProc.Signal, INFINITE);
enfin
LeaveCriticalSection(ThreadLock);
fin;
La plus grande différence entre le code ci-dessus et le code original est que WaitForSingleObject est également inclus dans les restrictions de la section critique. Cela semble n’avoir aucun impact, et cela simplifie grandement le code, mais est-ce vraiment possible ?
En fait non !
Parce que nous savons qu'après la section Entrée critique, si d'autres threads souhaitent entrer à nouveau, ils seront suspendus. La méthode WaitFor suspendra le thread en cours et ne sera pas réveillée tant qu'elle n'aura pas attendu SetEvent d'autres threads. Si le code est modifié comme ci-dessus, si le thread SetEvent doit également entrer dans la section critique, un blocage se produira (pour la théorie du blocage, veuillez vous référer aux informations sur les principes du système d'exploitation).
Le blocage est l’un des aspects les plus importants de la synchronisation des threads !
Enfin, l'événement créé au début est libéré. Si la méthode synchronisée renvoie une exception, l'exception sera à nouveau levée ici.
(à suivre)