This entry is part 3 of 3 in the series: Programación Multihilo en Delphi

Introducción

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.

Primitivas de sincronización de Windows

Handles

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.

WaitForSingleObject y WaitForMultipleObjects

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.

Mutex

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:

function CreateMutex(lpMutexAttributes: PSecurityAttributes;
                     bInitialOwner: BOOL; 
                     lpName: PChar): THandle;

  • lpMutexAttributes es un puntero a una estructura de tipo SECURITY_ATTRIBUTES que nos permite especificar los permisos de seguridad del objeto creado (en este caso del mutex). Si este puntero se define como nil al mutex se le asigna un descriptor de seguridad por defecto
  • bInitialOwner es un boolean que indica si el objeto que crea el mutex además es su dueño (es decir, si tiene adquirido el mutex nada más crearlo
  • lpName es el nombre del mutex

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.

function AbrirMutexConNombre(nombre : string) : integer; var

 mutex : THandle;
 waitRes : integer;

begin

 // Intentamos abrir el mutex, el parametro MUTEX_MODIFY_STATE
 // indica el nivel de acceso que deseamos obtener, en este caso,
 // deseamos un nivel de acceso que nos permita hacer un realease
 // sobre el mutex, el parametro SYNCHRONIZE es un modificador
 // de acceso que indica que querremos esperar sobre el mutex
 mutex := OpenMutex(SYNCHRONIZE OR MUTEX_MODIFY_STATE,true,nombre);
  if mutex > 0 then
  begin
    waitRes := WaitForSingleObject(mutex,3000);
    case waitRes of
      WAIT_TIMEOUT:
        result := -1; // TimeOUT;
      WAIT_OBJECT_0:
        begin
          ProcesarDatos; // Tenemos el mutex
          ReleaseMutex(mutex); // Hemos acabado
          CloseHandle(mutex);
          result := 0;
        end;
      else begin
        CloseHandle(mutex);
        raise Exception.Create('Error al esperar en el mutex');
      end;
    end;
  else // No se pudo abrir el mutex !! Con GetLastError podríamos ver el error
    raise Exception.Create('Error al abrir el mutex');

end;

Por último para cerrar el mutex (para liberar su memoria) debemos realizar una llamada a CloseHandle como ya se ha indicado.

Semaforos

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:

function CreateSemaphore(lpSemaphoreAttributes: PSecurityAttributes;
                         lInitialCount, lMaximumCount : LONGINT; 
                         lpName: PChar): THandle;

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.

Otros mecanismos de sincronización de windows

Eventos

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.

Secciones críticas

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.

Waitable Timers

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.

Un ejemplo

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.

interface type TTicketManager = class

private
  FMutex : THandle;
  FTicketSemaphore : THandle;
  FTickets : Array of TTicket;
  FFreeTickets : Array of boolean;
  // Inicializa un ticket dado
  function IniciaTicket(var ticket : TTicket) : boolean;
  procedure ObtenerTicketLibre(var ticket : TTicket);
public
  constructor Create(ATicketNumber : integer);
  destructor Destroy; override;

  // 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; 

Series Navigation

<< Programación multihilo en Delphi. TThread y sincronización básica