Clase de hilo en Delphi
Raptor[Estudio mental]
http://mental.mentsu.com
cuatro
La Sección Crítica (CriticalSection) es una tecnología para la protección del acceso a datos compartidos. En realidad, es equivalente a una variable booleana global. Pero su funcionamiento es diferente. Solo tiene dos operaciones: Entrar y Salir. Sus dos estados también se pueden considerar como Verdadero y Falso, que indican si se encuentra en la sección crítica. Estas dos operaciones también son primitivas, por lo que se pueden utilizar para proteger los datos compartidos de violaciones de acceso en aplicaciones multiproceso.
El método para usar secciones críticas para proteger los datos compartidos es muy simple: llame a Enter para configurar el indicador de la sección crítica antes de cada acceso a los datos compartidos, luego opere los datos y finalmente llame a Leave para salir de la sección crítica. Su principio de protección es el siguiente: después de que un hilo ingresa a la sección crítica, si otro hilo también quiere acceder a los datos en este momento, encontrará que un hilo ya ingresó a la sección crítica al llamar a Enter, y luego este hilo será Cuelgue y espere a que el hilo que se encuentra actualmente en la sección crítica llame a Dejar para salir de la sección crítica. Cuando otro hilo complete la operación y llame a Dejar para salir, este hilo se despertará, establecerá el indicador de la sección crítica y comenzará a operar con datos. evitando así el acceso.
Tomando el InterlockedIncrement anterior como ejemplo, usamos CriticalSection (API de Windows) para implementarlo:
var
Crítico entrelazado: TRTLCriticalSection;
Procedimiento InterlockedIncrement (var aValue: entero);
Comenzar
EnterCriticalSection(InterlockedCrit);
Inc(unValor);
LeaveCriticalSection(InterlockedCrit);
Fin;
Ahora mira el ejemplo anterior:
1. El hilo A ingresa a la sección crítica (suponiendo que los datos sean 3)
2. El hilo B ingresa a la sección crítica. Debido a que A ya está en la sección crítica, B se suspende.
3. El hilo A agrega uno a los datos (ahora 4)
4. El hilo A abandona la sección crítica y activa el hilo B (los datos actuales en la memoria son 4)
5. El hilo B se activa y agrega uno a los datos (ahora son 5)
6. El hilo B sale de la sección crítica y los datos actuales son correctos.
Así es como las secciones críticas protegen el acceso a los datos compartidos.
En cuanto al uso de secciones críticas, una cosa a tener en cuenta es el manejo de excepciones durante el acceso a los datos. Porque si ocurre una excepción durante la operación de datos, la operación Dejar no se ejecutará. Como resultado, el hilo que debería activarse no se activará, lo que puede hacer que el programa deje de responder. En términos generales, el enfoque correcto es utilizar secciones críticas de la siguiente manera:
Ingresar sección crítica
Intentar
//Operar datos de secciones críticas
Finalmente
Dejar sección crítica
Fin;
Lo último que hay que tener en cuenta es que Event y CriticalSection son recursos del sistema operativo, que deben crearse antes de su uso y liberarse después de su uso. Por ejemplo, un Evento global: SyncEvent y una Sección Crítica global: TheadLock utilizados por la clase TThread se crean y publican en InitThreadSynchronization y DoneThreadSynchronization, y se llaman en la unidad de Inicialización y Finalización de Clases.
Dado que las API se utilizan para operar Event y CriticalSection en TThread, la API se utiliza como ejemplo anterior. De hecho, Delphi ha proporcionado una encapsulación de ellas en la unidad SyncObjs, son la clase TEvent y la clase TCriticalSection respectivamente. El uso es casi el mismo que el método anterior de uso de API. Debido a que el constructor de TEvent tiene demasiados parámetros, para simplificar, Delphi también proporciona una clase de evento inicializada con parámetros predeterminados: TSimpleEvent.
Por cierto, permítanme presentarles otra clase utilizada para la sincronización de subprocesos: TMultiReadExclusiveWriteSynchronizer, que se define en la unidad SysUtils. Hasta donde yo sé, este es el nombre de clase más largo definido en Delphi RTL. Afortunadamente, tiene un alias corto: TMREWSync. En cuanto a su uso, creo que puedes saberlo con solo mirar el nombre, así que no diré más.
Con el conocimiento preparatorio previo sobre Event y CriticalSection, podemos comenzar oficialmente a discutir Synchronize y WaitFor.
Sabemos que Synchronize logra la sincronización de subprocesos colocando parte del código en el subproceso principal para su ejecución, porque en un proceso solo hay un subproceso principal. Primero veamos la implementación de Synchronize:
procedimiento TThread.Synchronize(Método: TThreadMethod);
comenzar
FSynchronize.FThread: = Uno mismo;
FSynchronize.FSynchronizeException: = nulo;
FSynchronize.FMethod := Método;
Sincronizar(@FSyncronizar);
fin;
donde FSynchronize es un tipo de registro:
PSynchronizeRecord = ^TSynchronizeRecord;
TSynchronizeRecord = grabar
FThread: TObject;
Método F: Método TThread;
FSynchronizeException: TObject;
fin;
Se utiliza para el intercambio de datos entre subprocesos y el subproceso principal, incluidos los objetos de clase de subproceso entrante, los métodos de sincronización y las excepciones que ocurren.
Una versión sobrecargada se llama en Synchronize, y esta versión sobrecargada es bastante especial, es un "método de clase". El llamado método de clase es un método miembro de clase especial. Su invocación no requiere la creación de una instancia de clase, sino que se llama a través del nombre de la clase como un constructor. La razón por la que se implementa utilizando un método de clase es que se puede llamar incluso cuando no se crea el objeto de subproceso. Sin embargo, en la práctica, se utiliza otra versión sobrecargada (también un método de clase) y otro método de clase StaticSynchronize. Aquí está el código para esta sincronización:
procedimiento de clase TThread.Synchronize(ASyncRec: PSSynchronizeRecord);
var
Sincronización: TSyncProc;
comenzar
si GetCurrentThreadID = MainThreadID entonces
Método ASyncRec.FMétodo
demás
comenzar
SyncProc.Signal: = CreateEvent (nulo, verdadero, falso, nulo);
intentar
EnterCriticalSection(ThreadLock);
intentar
si SyncList = nulo entonces
ListaSincronizada := TList.Create;
SyncProc.SyncRec := ASyncRec;
SyncList.Add(@SyncProc);
Evento de sincronización de señal;
si está asignado (WakeMainThread) entonces
WakeMainThread(SyncProc.SyncRec.FThread);
LeaveCriticalSection(ThreadLock);
intentar
WaitForSingleObject(SyncProc.Signal, INFINITO);
finalmente
EnterCriticalSection(ThreadLock);
fin;
finalmente
LeaveCriticalSection(ThreadLock);
fin;
finalmente
CloseHandle(SyncProc.Signal);
fin;
si está asignado (ASyncRec.FSynchronizeException), genere ASyncRec.FSynchronizeException;
fin;
fin;
Este código es un poco más largo, pero no es demasiado complicado.
La primera es determinar si el hilo actual es el hilo principal. Si es así, simplemente ejecute el método de sincronización y regrese.
Si no es el hilo principal, está listo para iniciar el proceso de sincronización.
Los datos de intercambio de subprocesos (parámetros) y un identificador de eventos se registran a través de la variable local SyncProc. La estructura de registro es la siguiente:
TSyncProc=registro
SyncRec: PSynchronizeRecord;
Señal: THhandle;
fin;
Luego cree un evento, luego ingrese a la sección crítica (a través de la variable global ThreadLock, porque solo un subproceso puede ingresar al estado Sincronizar al mismo tiempo, por lo que puede usar la variable global para registrar) y luego almacene los datos grabados en el Lista SyncList (si esta Si la lista no existe, créela). Se puede ver que la sección crítica de ThreadLock es proteger el acceso a SyncList. Esto se verá nuevamente cuando se presente CheckSynchronize más adelante.
El siguiente paso es llamar a SignalSyncEvent. Su código se introdujo antes al introducir el constructor TThread. Su función es simplemente realizar una operación Set en SyncEvent. El propósito de este SyncEvent se detallará más adelante cuando se presente WaitFor.
La siguiente es la parte más importante: llamar al evento WakeMainThread para operaciones de sincronización. WakeMainThread es un evento global de tipo TNotifyEvent. La razón por la que los eventos se usan para el procesamiento aquí es porque el método Synchronize esencialmente coloca el proceso que necesita sincronizarse en el hilo principal para su ejecución a través de mensajes. No se puede usar en algunas aplicaciones sin bucles de mensajes (como Consola o DLL). , así que utilice este evento para procesar.
El objeto de la aplicación responde a este evento. Los dos métodos siguientes se utilizan para configurar y borrar la respuesta al evento WakeMainThread (de la unidad de Formularios):
procedimiento TApplication.HookSynchronizeWakeup;
comenzar
Clases.WakeMainThread:= WakeMainThread;
fin;
procedimiento TApplication.UnhookSynchronizeWakeup;
comenzar
Classes.WakeMainThread: = nil;
fin;
Los dos métodos anteriores se llaman en el constructor y destructor de la clase TApplication respectivamente.
Este es el código que responde al evento WakeMainThread en el objeto Aplicación. El mensaje se envía aquí. Utiliza un mensaje vacío para lograr esto.
procedimiento TApplication.WakeMainThread(Remitente: TObject);
comenzar
PostMessage (identificador, WM_NULL, 0, 0);
fin;
La respuesta a este mensaje también se encuentra en el objeto Aplicación; consulte el siguiente código (elimine las partes irrelevantes):
procedimiento TApplication.WndProc(var Mensaje: TMessage);
…
comenzar
intentar
…
con mensaje hacer
mensaje de caso de
…
WM_NULL:
ComprobarSincronizar;
…
excepto
HandleException (auto);
fin;
fin;
Entre ellos, CheckSynchronize también se define en la unidad Clases. Dado que es relativamente complejo, no lo explicaremos en detalle por el momento. Solo sepa que es la parte que maneja específicamente la función Sincronizar. código.
Después de ejecutar el evento WakeMainThread, salga de la sección crítica y luego llame a WaitForSingleObject para comenzar a esperar el evento creado antes de ingresar a la sección crítica. La función de este Evento es esperar a que finalice la ejecución de este método de sincronización. Esto se explicará más adelante al analizar CheckSynchronize.
Tenga en cuenta que después de WaitForSingleObject, vuelve a ingresar a la sección crítica, pero sale sin hacer nada. Parece que no tiene sentido, ¡pero es necesario!
Porque entrar y salir en la sección crítica debe corresponder estrictamente uno a uno. Entonces se puede cambiar a esto:
si está asignado (WakeMainThread) entonces
WakeMainThread(SyncProc.SyncRec.FThread);
WaitForSingleObject(SyncProc.Signal, INFINITO);
finalmente
LeaveCriticalSection(ThreadLock);
fin;
La mayor diferencia entre el código anterior y el código original es que WaitForSingleObject también está incluido en las restricciones de la sección crítica. Parece no tener ningún impacto y simplifica enormemente el código, pero ¿es realmente posible?
De hecho, ¡no!
Porque sabemos que después de la sección Ingresar crítica, si otros hilos quieren ingresar nuevamente, serán suspendidos. El método WaitFor suspenderá el hilo actual y no se activará hasta que espere el SetEvent de otros hilos. Si el código se cambia a lo anterior, si el subproceso SetEvent también necesita ingresar a la sección crítica, se producirá un punto muerto (para conocer la teoría del punto muerto, consulte la información sobre los principios del sistema operativo).
¡El punto muerto es uno de los aspectos más importantes de la sincronización de subprocesos!
Finalmente, se libera el evento creado al principio. Si el método sincronizado devuelve una excepción, la excepción se generará nuevamente aquí.
(continuará)