[series-info:center]
Probablemente la parte más compleja al programar una aplicación con varios hilos es la sincronización entre estos tanto en el acceso a los datos compartidos como en el correcto orden de ejecución que deben seguir.
Uno de los problemas de desarrollar una aplicación multihilo es que son bastante dificiles de depurar puesto que los hilos van entrando y saliendo de ejecución conforme se va acabando su tiempo de ejecución de forma que, al depurar, el depurador va saltando de una linea de código a otra dependiendo de la tarea que vaya estando en la CPU así que siempre hay que tener mucho cuidado al escribir el código por que, si vas a tener que depurarlo, vas a pasarte un buen rato en ello (por no hablar del hecho de que los hilos no tienen porqué, y probablemente no lo haga, ejecutar en el mismo orden y con los mismos tiempos en dos ejecuciones consecutivas).
En esta parte trataremos los objetos de sincronismo más importante que proporciona Delphi mientras que en el siguiente nos centraremos en las primitivas de sincronización que proporciona la API de windows.
Una sección crítica representa una sección de código que tan solo puede ser ejecutada por una tarea a la vez, a todos los efectos es como declarar toda una sección de código como atómica.
Delphi provee de una clase específica para declarar una sección crítica, la clase TCriticalSection ubicada en la unidad SyncObjs. Para entrar en la sección crítica establecemos una llamada al metodo enter (o aquire) y para salir de la sección crítica utilizaremos una llamada al metodo leave.
La principal ventaja de una sección crítica es que es relativamente fácil de implementar, tan solo es necesario declarar la clase, crearla y "blindar" aquellas secciones de código que sean conflictivas de forma que podemos evitar la llamada al metodo syncronize (que bloquea la ejecución de la tarea hasta que el hilo principal puede parar para ejecutarla) y es mucho más lento.
La principal desventaja es que es un metodo relativamente tosco, provee de exclusión mutua en la ejecución de cierta sección de código de forma que podemos evitar que dos tareas entren a la vez pero no tenemos ningún control sobre que tareas necesitan hacer exclusión mútua y cuales no, por otro lado debemos crear una sección crítica para cada región de código que debamos proteger (la mayor parte de los objetos predefinidos de delphi tienen esta limitación pero ya veremos un metodo para implementar algo más flexible).
En determinadas ocasiones solo necesitamos proteger una determinada región de datos ante su acceso desde varias tareas de forma que varios hilos puedan leer un determinado valor siempre y cuando no haya nadie escribiendo en él (puesto que como ya vimos dicha operación puede ser conflictiva) pero un escritor deberá tener un acceso exclusivo.
El siguiente fragmento de código representa una función de asignación de un puntero a un objeto dentro de una clase predefinida item (que por ejemplo puede almacenar varios tipos de valores, entre ellos punteros a objeto).
procedure TItem.UpdateObject(newObject : TObject);
var
oldObject : TObject;
begin
self.Type = itm_object;
oldObject := TObject(self.Ptr);
if Assigned(oldObject) then
FreeAndNil(oldObject);
self.Ptr := newObject;
end;
Podemos observar que, de acceder a la función varios hilos simultaneamente puede producirse un conflicto si se produce un cambio de contexto justo después del FreeAndNil pero antes de la asignación de self.Ptr, Ptr estará apuntando a una dirección de memoria invalida y si otra tarea accede en ese momento a la función GetObj obtendrá una dirección erronea. Esta claro que el miembro Ptr de la clase es crítico y debe ser protegido.
Envolver las zonas críticas mediante una sección crítica sería una opción.
procedure TItem.UpdateObject(newObject : TObject);
var
oldObject : TObject;
begin
self.Type = itm_object;
oldObject := TObject(self.Ptr);
critical.Enter;
if Assigned(oldObject) then
FreeAndNil(oldObject);
self.Ptr := newObject;
critical.Leave;
end;
Aunque esta solución es válida presenta un problema si (como suele ser normal) el numero de accesos de lectura es muy superior al numero de accesos de escritura. Cuando dos tareas quieren obtener el valor del objeto al que apunta Ptr deben "pelear" por entrar en la sección crítica aunque, mientras nadie intente escribir en la variable no existe ningún problema por que dos hilos obtengan el valor de la variable.
Para prevenir este problema existe otro objeto de sincronización en Delphi que permite algo más de control sobre lo que vamos a hacer dentro del area protegida. El TMultiReadExclusiveWriteSynchronizer. Los cuatro metodos que nos interesan del objeto son BeginRead, EndRead, BeginWrite, EndWrite, que, como su nombre indica, nos permiten indicar el inicio y final de una operación de lectura o escritura respectivamente.
procedure TItem.UpdateObject(newObject : TObject);
var
oldObject : TObject;
begin
self.Type = itm_object;
oldObject := TObject(self.Ptr);
MReaderOWriter.BeginWrite
if Assigned(oldObject) then
FreeAndNil(oldObject);
self.Ptr := newObject;
MReaderOWrite.EndWrite;
end;
De esta forma cuando un hilo quiera escribir el puntero esperará a que todos los demás hilos terminen de leer y comenzará a escribir y durante el tiempo que este escribiendo ningún otro hilo podrá entrar ni para escribir ni para leer mientras que si varios hilos quieren leer el valor del puntero podrán hacerlo de forma simultanea.
Cuando hablamos de eventos en Delphi podemos cometer el error de identificar esos eventos con las acciones asociadas a el clickeo de un botón por parte del usuario o la introducción de texto en un edit box, fundamentalmente por que la terminología normal es llamar a las funciones que manejan dichos "eventos" se llaman manejadores de evento y por que además Delphi define algunos delegados con nombres que llevan al error (el tipo TNotifyEvent a pesar de su nombre no es un evento).
Cuando hablamos de eventos en el contexto multihilo no estamos hablando de esto. Internamente Delphi tiene un manejador de mensajes que redirige los mensajes de windows (tales como el WM_MOUSEDOWN correspondiente a un click del mouse) a los controles adecuados y, en su caso, invoca los manejadores asociados a dichos mensajes pero esto es un concepto totalmente diferente del que vamos a tratar.
Los metodos que hemos visto hasta el momento constituyen una forma de proteger los datos compartidos mientras que los eventos constituyen un metodo de sincronización.
Tal y como su nombre indica los eventos nos permiten hacer que un hilo de ejecución se quede a la espera de que ocurra un determinado evento que active su ejecución, por ejemplo si tenemos una tarea que se encarga de procesar la información que llega de un socket el evento asociado sería la llegada de información por ese socket o quizá nuestro hilo este esperando a que otro hilo termine su ejecución (o que llegue a un determinado punto de su ejecución) tras lo cual se producirá el evento por el que estará esperando nuestro hilo.
Por tanto podemos decir que existe una diferenciación fundamental a la hora de tratar con eventos, por un lado esta quien invoca el evento y por otro lado esta el hilo o los hilos que están esperando a que se produzca dicho evento.
El objeto que nos interesa para implementar nuestros eventos es la clase TEvent que es en realidad un envoltorio que proporciona Delphi para el objeto event de windows. De la clase TEvent nos interesan fundamentalmente:
Uno de los casos más habituales en los que es recomendable el uso de eventos es en los programas en los que existe un productor y un consumidor, de forma que el evento que el consumidor espera es la presencia de datos que consumir y el agente que activa el evento es el productor cada vez que proporciona un dato.
El evento siempre se utiliza de la misma forma, mediante llamadas a los metodos citados anteriormente, sin embargo hay varias formas de 'presentarlo'.
Una opción es ocultar la existencia del evento de forma que cualquier visor externo solo perciba que se trata de una llamada bloqueante, es decir, una llamada que puede suponer el bloqueo del hilo llamante.
implementation
constructor TProductor.Create(createSuspended : boolean);
begin
// Aqui se guardarán los datos
internalData := TStringList.Create;
// Crear el evento, seguridad por defecto, reseteo automático
Event := TEvent.Create(nil,false,false,'');
end;
function TProductor.LeerCadena(timeout: integer) : string;
var
res : integer;
begin
if timeout = 0 then
res := Event.WaitFor(INFINITE)
else
res := Event.WaitFor(timeout);
if res = wrSignaled then
begin
result := internalData[0];
internalData.Delete(0);
if internalData.Count > 0 then
Event.SetEvent; // Quedan datos, relanzamos el evento
end
else if res = wrTimeout then
result := 'timeout'
else
result := 'error';
end;
procedure TProductor.Execute;
begin
while ot Self.Terminated do
begin
ReadDataFromFile;
FEvent.SetEvent;
sleep(1000); // Dormir un segundo
end;
end;
La otra opción sería dar acceso directamente al consumidor al objeto TEvent de forma que antes de realizar una llamada a la función de lectura el consumidor espera la activación del evento activamente. Esta opción permite un control más directo de lo que vamos a hacer y resulta indispensable si deseamos realizar acciones más complejas que un simple WaitFor de un evento (como por ejemplo esperar por varios eventos en vez de uno).
property Event : TEvent read FEvent;
implementation
function TProductor.LeerCadena : string;
begin
if FInternalData.Count > 0 then
result := FInternalData[0]
else
raise Exception.Create('Intento de leer sin esperar el evento');
end;
Evidentemente este segundo metodo es más 'arriesgado' por llamarlo de alguna forma, es decir, no se garantiza que un hilo haga un WaitFor antes de realizar la lectura (de ahí la excepción). Sin embargo proporciona más control al dar acceso al objeto TEvent.