[series-info:left]
En las anteriores partes, en los dos primeros articulos, hemos visto como crear y sincronizar hilos usando las clases predefinidas de Delphi. Estas clases son en realidad un encapsulamiento de las primitivas que nos proporciona windows para el control de hilos de ejecución pero hay determinadas cosas que no se pueden hacer con ellas y debemos recurrir directamente a los servicios que nos proporciona el sistema operativo.
Las funciones de creación de objetos de windows son bastante similares entre si y tienen la "peculiaridad" de no devolver una instancia del objeto tal y como podemos estar acostumbrados en Delphi sino que devuelve siempre un THandle que no es ni más ni menos que un cardinal. Este cardinal es en realidad el identificador que tiene windows para el objeto que ha creado internamente y que el mismo mantiene.
De esta forma, para aquellos elementos con nombre, podemos acceder a ellos desde cualquier proceso que este ejecutando (no solamente desde los hilos del proceso que lo crea) siempre y cuando tengamos los permisos suficientes (por ejemplo un proceso ejecutando bajo el identificador de un usuario de windows tendrá limitaciones a la hora de acceder a un objeto creado por un administrador).
Internamente windows mantiene un solo handle por objeto y un mecanismo de conteo referencial de forma que solo cuando el numero de referencias a un objeto llega a cero se borra el objeto. Generalmente existirá siempre un creador del objeto (mediante a alguna de las primitivas de creación como CreateProcess o CreateMutex) y una serie de hilos que obtendran el handle del objeto dado su nombre (mediante llamadas a OpenProcess o OpenMutex etc). Cada uno de esos hilos debe realizar su propia llamada a la función CloseHandle una vez termine de utilizar el objeto de forma que Windows sepa que ya no vamos a necesitar ese objeto y que, por nuestra parte, puede liberarlo.
Windows proporciona dos funciones de espera sobre objetos de sincronización, WaitForSingleObject y WaitForMultipleObjects. WaitForSingleObject espera para obtener el objeto especificador por un handle mientras que WaitForMultipleObjects espera para obtener alguno (o todos) los objetos especificados por un array de handles, sin embargo hay otra diferencia entre ambas llamadas.
En una llamada a WaitForSingleObject la tarea llamante se bloquea y deja de procesar mensajes, esto es, si la tarea esta encargada de manejar algún mensaje de windows que llegue a una ventana no los procesará hasta que no retorne de la llamada Wait. En cambio en una llamada a WaitForMultipleObjects no tenemos esa limitación, es decir, la tarea llamante seguirá procesando los mensajes mientras espera a que el objeto este listo.
Un mutex constituye una de las primitivas de sincronización más simple. Su funcionamiento es muy similiar al de la sección crítica que ya hemos visto exceptuando que los mutex pueden crearse con o sin nombre de forma que, si se crean con nombre, puede ser accedidos desde distintos procesos (no solo desde las distintas tareas dentro de un proceso).
Cuando creamos un mutex estamos creando una primitiva que podríamos equiparar con un testigo, solo un hilo o proceso puede poseer (tener) el testigo a la vez de forma que cuando un hilo solicita el testigo y este esta libre lo adquiere, si el testigo esta siendo usado por algun otro hilo, entonces el hilo que quiere adquirir el testigo se bloquea a la espera de que este quede libre.
La definición del constructor del mutex admite tres argumentos:
Para utilizar el mutex debemos hacer uso de las funciones de espera WaitForSingleObject o WaitForMultipleObject sobre el handle que hemos obtenido. Cuando obtenemos un mutex se dice que somos su dueño, y, mientras lo seamos, ningún otro hilo puede obtener ese mutex. Para liberar (dejar de ser dueños) el mutex deberemos llamara a la función ReleaseMutex (con el handle al mutex como parametro) que hará que el mutex que libre.
En ocasiones querremos utilizar un mutex ya creado en el sistema. Para hacer uso de dicho mutex podemos obtener su handle realizando una llamada a la función OpenMutex.
Por último para cerrar el mutex (para liberar su memoria) debemos realizar una llamada a CloseHandle como ya se ha indicado.
Un semaforo es una primitiva muy similar al mutex exceptuando el numero de testigos, para hacer un simil podríamos imaginar un semaforo como una estantería con un determinado numero de testigos, según van entrando hilos van cojiendo un testigo hasta que no queden, momento en el cual los procesos comenzarán a tener que esperar. En resumen, un semaforo es una estructura de sincronización que permite dejar entrar a un determinado numero máximo de hilos de forma simultanea y, una vez alcanzado el "aforo máximo" no permite entrar a nadie más hasta que no salga alguno de ellos.
El código para crear un semaforo es muy similar al código de creación de un mutex:
Los parametros de seguridad y el nombre del semaforo son iguales a los parametros que ya vimos para el constructor del mutex. Los dos parametros lInitialCount y lMaximumCount definen el numero inicial de testigos usados y el numero máximo de testigos en el semaforo (siguiendo con el simil anterior).
El mecanismo para obtener un handle dado el nombre del semaforo así como para liberar el handle es exactamente el mismo que para el caso del mutex, exceptuando que para abrir el mutex la llamada a realizar es a la función OpenSemaphore.
Los eventos que proporciona windows son muy similares a la clase TEvent que ya vimos en la segunda parte de esta serie, no voy a darles muchas vueltas aqui puesto que su funcionamiento quedo explicado de una forma bastante clara y creo que la clase TEvent envuelve bastante bien la funcionalidad proporcionada por windows de forma que sale más rentable crear una instancia de la clase que acceder mediante las primitivas de windows.
Windows también nos proporciona secciones críticas, que son ni más ni menos que mutex sin nombre (según la propia explicación en el msdn) y que quedan perfectamente encapsuladas en el TCriticalSection de Delphi que ya se vio en la segunda parte de esta serie y por lo tanto tampoco me voy a meter con ellas.
Los WaitableTimer son similares a los objetos TTimer de Delphi. Tienen dos modos de uso, por decirlo de alguna forma, sincrono (en el cual esperamos activamente a que se cumpla el tiempo indicado mediante una llamada a WaitForSingleObject) y el modo asincrono en el cual podemos pasar un puntero a una función de callback que se activará cuando pase el tiempo del timer. Toda esta funcionalidad la proporciona bastante bien la clase TTimer, en cualquier caso siempre hay que tener en cuenta que nuestro código sea reentrante si vamos a andar trasteando con señales asincronas.
Vamos a ver un hipotético ejemplo con una clase que proporciona "tickets". Cada ticket va a representar un identificador de acceso a algún recurso compartido (podría ser un puntero, un objeto, una sessión de la base de datos) ... Utilizaremos un semaforo para controlar que no proporcionemos más tickets de los que tenemos y un mutex para garantizar un uso exlusivo del controlador que despacha los tickets.
{ Obtiene un ticket garantizando que el llamante
es el único propietario del ticket }
function ObtenerTicket(timeOut : integer; var ticket : TTicket) : integer;
{ Permite devolver un ticket una vez que se deja
de usar }
function DevolverTicket(ticket TTicket);
end;
implementation
constructor TTicketManager.Create(ATicketNumber : integer);
begin
// Crear el mutex para hacer acceso exclusivo al array
FMutex := CreateMutex(nil,false,'');
FTicketSemaphore := CreateSemaphore(nil,0,ATicketNumber,'');
// Crear los tickets
SetLength(FTickets,ATicketNumber);
SetLength(FFreeTickets,ATicketNumber);
FFirstFreeTicket := 0;
end;
destructor TTicketManager.Destroy;
begin
// Cerrar el mutex y el semaforo
CloseHandle(FMutex);
CloseHandle(FTicketSemaphore);
end;
procedure ObtenerTicketLibre(var ticket : TTicket);
var
i : integer;
begin
// Obtener el ticket
ticket := FTickets[FFirstFreeTicket];
// Buscar el primer ticket libre
WaitForSingleObject(FMutex,INFINITE);
FFreeTickets[FFirstFreeTicket];
for i := Low(FFreeTickets) to High(FFreeTickets) do
begin
if FFreeTickets[i] then
begin
FFirstFreeTicket := i;
break;
end;
end;
ReleaseMutex(FMutex);
end;
function TTicketManager.ObtenerTicket(timeOut : integer; var ticket : TTicket) : integer;
var
res : DWORD;
begin
res := WatiForSingleObject(FTicketSemaphore,timeOut);
case res of
WAIT_OBJECT_0:
begin
ObtenerTicketLibre(ticket);
IniciaTicket(ticket);
end;
WAIT_TIMEOUT:
begin
result := -1; // Timeout en la espera
end;
else
raise Exception.Create('Ocurrió un error al esperar por el ticket');
end;
end;