6.3. EL PAQUETE DE HILOS DEL SUBSISTEMA Win32.

  1. Características generales.
  2. Gestión de procesos e hilos.
  3. Sincronización de hilos.
    1. Sincronización con SeccionesCríticas.
    2. Sincronización con Objetos.
    3. Sincronización con Mútex.
    4. Sincronización con Semáforos.
    5. Sincronización con Sucesos.
    6. Suspensión de hilos.
  4. Comparación de Win32 Threads y POSIX Threads.

El sistema operativo Windows NT tiene varios subsistemas de entorno de sistemas operativos. Uno de ellos es el subsistema Win32, que es el subsistema empleado por las aplicaciones nativas de Windows NT. El subsistema Win32 permite multiproceso y multihilado. Los hilos de nivel usuario se corresponden con hilos de nivel kernel. El paquete implementa hilos a nivel del usuario, pero tienen un soporte a nivel del kernel NT.

A continuación se hará una descripción general del paquete para el programador a nivel usuario, así como las herramientas que proporciona el paquete para programar aplicaciones multihilo en el entorno Win32.

Características generales.

El paquete de hilos que se implementa en Win32 es un paquete de nivel usuario que proporciona funciones para que gestión de múltiples hilos así como sincronización entre ellos. Sin embargo, el paquete de hilos de Win32 no sigue el estándar POSIX 1003.1.c sobre hilos, por lo cual para programar con este paquete es necesario aprender las nuevas funciones de la librería proporcionada por el paquete, y por lo tanto nuestra aplicación no será portable a otros sistemas. Sin embargo, últimamente han aparecido una serie de librerías para Wi32 que implementan el estándar POSIX Threads apoyándose en los hilos de Win32.

El API de la biblioteca de hilos de Win32 es idéntico para Windows NT y para Windows'95, excepto que la implementación del paquete es distinta para cada uno de los dos sistemas operativos de Microsoft. En este apartado se estudia la implementación sobre Windows NT, aunque la mayoría de las características son aplicables a Windows'95.

Gestión de procesos e hilos.

En el subsistema Win32 un proceso puede tener varios hilos. Cuando se crea un proceso, el sistema crea automáticamente el hilo primario, donde se ejecuta C Runtime, que posteriormente llama a la función de entrada al código del programa WinMain(). El hilo primario puede crear hilos adicionales, y mientras exista un hilo activo, el hilo primario y el proceso no terminan.

Para manejar los objetos, se emplean los handles o descriptores de objetos. Si un objeto del sistema ya existe, cualquier aplicación puede abrir un descriptor a él para utilizarlo, de esta forma el sistema incrementa un contador de uso del objeto, el objeto se comparte, sin duplicarle, y el sistema devuelve un descriptor, que sirve para identificar el objeto para el hilo que abre el descriptor. Cuando el hilo no necesita manipular más el objeto, llama a la función CloseHandle(HandleObjeto), que decrementa el contador de uso del objeto. Los descriptores de objetos son relativos al proceso, de tal forma que si dos hilos de procesos distintos abren descriptores al mismo objeto del sistema, estos descriptores serán diferentes y sólo válidos en los ámbitos de cada proceso.

Un proceso Win32 se crea desde un hilo de otro proceso mediante la función :

BOOL CreateProcess(
LPCTSTR                 lpszImageName,
LPCTSTR                 lpszCommandLine,
LPSECURITY_ATTRIBUTES   lpsaProcess,
LPSECURITY_ATTRIBUTES   lpsaThread,
BOOL                            fInheritHandles,
DWORD                           fdwCreate,
LPVOID                          lpvEnvironment,
LPTSTR                          lpszCurDir,
LPSTARTUPINFO                   lpsiStartInfo,
LPPROCESS_INFORMATION   lppiProcInfo
) ;


La finalización de un proceso Win32 se realiza cuando uno de los hilos del proceso llama a la siguiente función :

VOID ExitProcess(UINT fuExitCode) ;


Al finalizar un proceso, todos los hilos del proceso finalizan voluntariamente, se cierran todos los descriptores de objeto abiertos por el proceso, el estado del objeto proceso pasa a ser señalado, y el estado de terminación pasa al código de salida establecido en la llamada. La finalización de un proceso Win32 no implica la finalización de sus procesos hijo, a diferencia de lo que ocurre con otros subsistemas, como POSIX, y además no es liberado hasta que todas las referencias (descriptores) al objeto se han cerrado.

Un hilo puede crear otro hilo dentro del mismo proceso, mediante la función :

HANDLE CreateThread(
LPSECURITY_ATTRIBUTES   lpsaThread,
DWORD                           cbStack,
LPTHREAD_START_ROUTINE  lpStartAddr,
LPVOID                          lpvThreadParm,
DWORD                           fdwCreate,
LPDWORD                 lpIDThread
) ;


La función de creación devuelve un descriptor del hilo creado. Además la función de inicio del hilo debe tener el siguiente esquema :

DWORD WINAPI ThreadFunction(LPVOID lpvThreadParm) {
   DWORD dwResult = 0 ;
   /* Código del hilo */
   return (dwResult) ;
}


También debemos decir, que aunque los hilos comparten las variables globales, pueden disponer de memoria local privada, pero global a sus procedimientos, empleando funciones del sistema Win32. Cuando las variables globales son compartidas por varios hilos es necesario establecer una sincronización en su acceso.

Sincronización de hilos.

Cuando se necesita compartir recursos entre diversos hilos, es necesario que exista un mecanismo de sincronización, que se consigue mediante los objetos de sincronización. Existen objetos que cumplen una función en el sistema, pero que además sirven como objetos de sincronización, como los objetos proceso, hilo y archivo, y existen otros objetos cuya función es exclusivamente de sincronización, como los objetos semáforo y mutex. Cada objeto de sincronización tiene características especiales que lo hacen apropiado para distintas tareas.

Existen diversos mecanismos de sincronización entre hilos en Win32 que son presentados a continuación.

Sincronización con Secciones Críticas.

En Win32 se pueden definir secciones críticas dentro de un proceso, es decir, partes de código que requieren el acceso exclusivo a una serie de datos compartidos entre diversos hilos de un mismo proceso. El acceso a la sección crítica está controlado por el sistema operativo, que garantiza que un sólo hilo se encuentra en su interior en un momento dado. La sección crítica se implementa creando una estructura de datos del tipo CRITICAL_SECTION, global a todos los hilos implicados. En cada hilo la sección crítica viene definida por el conjunto de sentencias entre las llamadas a las siguientes funciones:

VOID EnterCriticalSection(LPCRITICAL_SECTIO lpCriticalSection) ;
VOID LeaveCriticalSection(LPCRITICAL_SECTIO lpCriticalSection) ;


A estas funciones se les pasa un puntero a la estructura de datos global que gestiona el acceso a la región crítica. Si algún hilo llama a EnterCriticalSecion() mientras otro hilo está dentro de la sección crítica y tiene el acceso bloqueado a la estructura, es suspendido por el sistema hasta que se libera la estructura. Un mismo proceso puede tener varias secciones críticas anidadas o implementar más de una sección crítica sobre la misma estructura, ya que se incrementa un contador de uso de la estructura, de tal forma que la estructura sólo es liberada cuando el contador de uso se decrementa hasta 0 mediante LeaveCriticalSection().

Un ejemplo de uso de secciones críticas es el siguiente :

/* Definición de variables globales */
int g_nIndex = 0 ;
const int MAX_TIMES = 1000 ;
DWORD g_dwTimes[MAX_TIMES] ;
CRITICAL_SECTION g_CriticalSection ;

/* Programa principal */
int WinMain(...) {
   HANDLE hThread[2] ;
   DWORD  IDThread[2] ;
   /* Inicializamos la sección crítica */
   InitializeCriticalSection(&g_CriticalSection) ;
   /* Creamos los hilos de trabajo */
   hThread[0] = CreateThread(NULL, 0, FirstThread,  NULL, 0, &IDThread[0]) ;
   hThread[1] = CreateThread(NULL, 0, SecondThread, NULL, 0, &IDThread[1]) ;
   /* Esperamos la finalización de los hilos de trabajo */
   WaitForMultipleObjects(2, hThread, TRUE, INFINITE) ;
   /* Cerramos los descriptores de los hilos */
   CloseHandle(hThread[0]) ;
   CloseHandle(hThread[1]) ;
   /* Eliminamos la sección crítica */
   DeleteCriticalSection(&g_CriticalSection) ;
}

/* Función correspondiente al Primer Hilo */
DWORD WINAPI FirstThread(LPVOID lpvThreadParm) {
   BOOL fDone = FALSE ;
   while (!fDone) {
        /* Solicitamos acceso a la región crítica */
        EnterCriticalSection(&g_CriticalSection) ;
        if (g_nIndex >= MAX_TIMES) {
           fDone = TRUE ;
        } else {
           g_dwTimes[g_nIndex] = GetTickCount() ;
           g_nIndex++ ;
        }
        /* Liberamos el acceso a la región crítica */
        LeaveCriticalSection(&g_CriticalSection) ;
   }
   return (0) ;
}

/* Función correspondiente al Segundo Hilo */
DWORD WINAPI SecondThread(LPVOID lpvThreadParm) {
   BOOL fDone = FALSE ;
   while (!fDone) {
        /* Solicitamos acceso a la región crítica */
        EnterCriticalSection(&g_CriticalSection) ;
        if (g_nIndex >= MAX_TIMES) {
           fDone = TRUE ;
        } else {
           g_nIndex++ ;
           g_dwTimes[g_nIndex-1] = GetTickCount() ;
        }
        /* Liberamos el acceso a la región crítica */
        LeaveCriticalSection(&g_CriticalSection) ;
   }
   return (0) ;
}


Las secciones críticas no son objetos de sincronización y son apropiadas para realizar el acceso a datos dentro de un mismo proceso, ya que son muy rápidas.

Sincronización con Objetos.

Los objetos de Win32 que se pueden emplear para sincronización son : procesos, hilos, archivos, notificación de cambio de archivo, entrada de consola, mutex, semáforos y sucesos. Su ventaja es que no se restringe la sincronización a hilos de un mismo proceso, sino que permiten sincronizar hilos de procesos distintos. Un objeto puede tener uno de dos estados posibles : señalado y no señalado.

Las principales funciones que emplean los hilos para la sincronización son :

DWORD WaitForSingleObject(HANDLE hObject, DWORD dwTimeout) ;


Espera a que el objeto especificado esté en el estado señalado.

Esta función devuelve uno de los siguientes valores :

DWORD WaitForMultipleObjects(HANDLE cObjects, LPHANDLE lpHandles,
BOOL bWaitAll, DWORD dwTimeout) ;

Espera a que uno o varios de los objetos especificados estén señalados.

Esta función devuelva valores similares a la anterior, pero teniendo en cuenta que la espera se realiza por múltiples objetos.

Además, estas funciones pueden producir un cambio de estado sobre los objetos que comprueban. Para objetos proceso e hilo no sucede nada, pero para objetos mútex, semáforo y suceso, pueden cambiar el estado del objeto, es decir, si el objeto es señalado provocando que otro hilo sea despertado, el objeto cambia a su estado no señalado.

Sincronización con Mútex.

Los mútex son objetos de sincronización parecidos a las secciones críticas, excepto que pueden ser empleados para sincronizar hilos de distintos procesos.

Un mútex se crea con la función :

HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpsaMutex, BOOL fInitialOwner,
LPTSTR lpszMutexName) ;


Una vez que el mútex está bloqueado, cualquier hilo que intente bloquear el mútex es suspendido hasta que el mútex es liberado. Para sincronizar varios hilos con un mútex, deben poseer descriptores al mismo objeto mútex. El descriptor al mútex se puede obtener de varias formas, como la herencia, el duplicado del descriptor, etc., aunque lo normal es emplear la función CreateMutex(), o bien la siguiente función :

HANDLE OpenMutex(DWORD fdwAccess, BOOL fInherit, LPTSTR lpszName) ;


Si el nombre del mútex no existe, se devuelve NULL, en otro caso se devuelve un descriptor al mútex.

Cuando un hilo propietario del mútex ya no lo necesita, puede liberarlo, indicando el descriptor del objeto mútex asociado mediante la función :

BOOL ReleaseMutex(HANDLE hMutex) ;


De esta forma se cambia el mútex del estado no señalado al estado señalado, y permite que otro hilo se adueñe del mútex.

El ejemplo anterior de las secciones críticas se podría plantear de la siguiente forma empleando mútex para establecer la sincronización de acceso a la regiones críticas:

/* Definición de variables globales */
int g_nIndex = 0 ;
const int MAX_TIMES = 1000 ;
DWORD g_dwTimes[MAX_TIMES] ;
HANDLE g_hMutex = NULL ;

/* Programa principal */
int WinMain(...) {
   HANDLE hThread[2] ;
   DWORD  IDThread[2] ;
   /* Creamos el mútex sin ser propietarios y sin nombre */
   g_hMutex = CreateMutex(NULL, FALSE, NULL) ;
   /* Creamos los hilos de trabajo */
   hThread[0] = CreateThread(NULL, 0, FirstThread,  NULL, 0, &IDThread[0]) ;
   hThread[1] = CreateThread(NULL, 0, SecondThread, NULL, 0, &IDThread[1]) ;
   /* Esperamos la finalización de los hilos de trabajo */
   WaitForMultipleObjects(2, hThread, TRUE, INFINITE) ;
   /* Cerramos los descriptores de los hilos */
   CloseHandle(hThread[0]) ;
   CloseHandle(hThread[1]) ;
   /* Eliminamos el mútex */
   CloseHandle(g_hMutex) ;
}

/* Función correspondiente al Primer Hilo */
DWORD WINAPI FirstThread(LPVOID lpvThreadParm) {
   BOOL fDone = FALSE ;
   DWORD dw ;
   while (!fDone) {
        /* Esperamos a que el mútex este libre */
        if ((dw = WaitForSingleObject(g_hMutex, INFINITE)) == WAIT_OBJECT_0) {
           if (g_nIndex >= MAX_TIMES) {
                fDone = TRUE ;
           } else {
                g_dwTimes[g_nIndex] = GetTickCount() ;
                g_nIndex++ ;
           }
           /* Liberamos el mútex */
           Release(g_hMutex) ;
        } else {
           break ; /* Se ha producido un error */
        }
   }
   return (0) ;
}

/* Función correspondiente al Segundo Hilo */
DWORD WINAPI FirstThread(LPVOID lpvThreadParm) {
   BOOL fDone = FALSE ;
   DWORD dw ;
   while (!fDone) {
        /* Esperamos a que el mútex este libre */
        if ((dw = WaitForSingleObject(g_hMutex, INFINITE)) == WAIT_OBJECT_0) {
           if (g_nIndex >= MAX_TIMES) {
                fDone = TRUE ;
           } else {
                g_nIndex++ ;
                g_dwTimes[g_nIndex-1] = GetTickCount() ;
           }
           /* Liberamos el mútex */
           Release(g_hMutex) ;
        } else {
           break ; /* Se ha producido un error */
        }
   }
   return (0) ;
}


En Win32, si un hilo propietario de un mútex bloqueado termina sin liberar el mútex, el sistema como conserva la pista del hilo propietario del mútex, fuerza al mútex al estado señalado, liberándolo, pero las funciones de sincronización del tipo Wait... del resto de hilos que esperaban por el mútex devuelven el valor WAIT_ABANDONED, que indica un estado anormal.

Sincronización con Semáforos.

Los semáforos son parecidos a los mútex, salvo que el sistema operativo no conserva la pista del hilo propietario del semáforo en cada momento. Por ello no es posible que un hilo espere por un semáforo, y otro hilo lo libere. Un semáforo permite que múltiples hilos puedan acceder a él simultáneamente ya que posee un contador de accesos. El semáforo está en el estado señalado cuando el contador es cero, y cada vez que un hilo llama a las funciones Wait... con el descriptor del objeto semáforo asociado, comprueba si el contador es mayor que ceros. Si es cero el sistema suspende al hilo que realizó la llamada Wait hasta que otro hilo libera el semáforo.

Un semáforo se crea mediante la siguiente función, que devuelve un descriptor al objeto semáforo asociado :

HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTE lpsaSem, LONG cSemInitial,
LONG cSemMax, LPTSTR lpszSemName) ;


El parámetro lpszSemName se puede utilizar en otros procesos para adquirir un descriptor de acceso al semáforo, mediante la función :

HANDLE OpenSemaphore(DWORD fdwAccess, BOOL fInherit, LPTSTR lpszName) ;


Si el nombre del mútex no existe, se devuelve NULL, en otro caso se devuelve un descriptor al semáforo.

Un semáforo se libera mediante la siguiente función :

BOOL ReleaseSemaphore(HANDLE hSem, LONG cRelease, LPLONG lplPrevious) ;


Los semáforos son útiles para administrar recursos limitados a una cantidad determinada que se deben compartir entre diversos hilos. El contador del semáforo llevará la cuenta del número de recursos disponibles.

Sincronización con Sucesos.

Los objetos suceso o evento son diferentes de los mútex y de los semáforos. Un objeto suceso se utiliza para indicar que alguna operación se ha completado. Los sucesos pueden ser de dos tipos : inicialización manual, que se emplean para indicar a varios hilo que se ha completado una operación, o autoinicialización, que se emplean para indicar lo mismo a un sólo hilo.

Los sucesos se crean con la función :

HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpsaEvent, BOOL fManualReset,
BOOL fInitialState, LPSTR lpszEventName) ;


La función devuelve un descriptor al suceso. El resto de hilos pueden acceder al objeto suceso creado mediante un descriptor al suceso, que se puede obtener de varias formas, como la herencia, el duplicado del descriptor, etc., aunque lo normal es emplear la función CreateEvent(), o bien la siguiente función :

HANDLE OpenEvent(DWORD fdwAccess, BOOL fInherit, LPTSTR lpszName) ;


Si el nombre del suceso no existe, se devuelve NULL, en otro caso se devuelve un descriptor al suceso.

El cierre de los sucesos se realiza pasando el descriptor del suceso a la función :

BOOL CloseHandle(HANDLE hSuceso) ;


Los sucesos con inicialización manual no cambian al estado señalado con la función Wait... por lo que todos los hilos que esperan por el suceso serán despertados y continuarán su ejecución. Un hilo pone en estado señalado a un objeto suceso mediante la siguiente función :

BOOL SetEvent(HANDLE hEvent) ;


Mientras que el hilo establece el estado no señalado al objeto suceso mediante la siguiente función :

BOOL ResetEvent(HANDLE hEvent) ;


Existe además una función que permite pasar al suceso al estado señalado y de inmediato al estado no señalado, lo que permite arbitrar, por ejemplo, la lectura de varios hilos a una base de datos, pero que se bloqueen cuando se realiza una escritura :

BOOL PulseEvent(HANDLE hEvent) ;


Los sucesos con autoinicialización tienen un comportamiento similar a los mútex y semáforos. Cuando un hilo llama a SetEvent para poner el suceso al estado señalado, éste permanece en dicho estado hasta que otro hilo que esperaba por el suceso se despierta. Justo antes de reactivar el hilo que estaba esperando, el sistema cambia automáticamente el suceso al estado no señalado. Así, sólo un hilo continúa su ejecución, que será seguramente el que posea mayor prioridad en ese momento.

Suspensión de hilos.

Los hilos se pueden suspender a sí mismos con otras funciones distintas de Wait..., que emplean otros métodos. Estas funciones son :

El hilo se suspende hasta que pasa el tiempo establecido en cMilliseconds. Si es 0, el hilo cede el resto de su quantum de tiempo asignado a otro hilo elegido por el sistema.

La función espera hasta que el proceso identificado por hProcess no tenga pendientes ninguna entrada en el hilo que creó la primera ventana de la aplicación.

Es similar a WaitForMultipleObjects, pero incorpora un nuevo parámetro dwWakeMask, que permite indicar que tipo de mensajes despertarán al hilo. Por ejemplo, un hilo puede suspenderse hasta que haya en la cola mensajes del teclado o ratón.

Estas funciones permiten cambiar el valor de una variable compartida por varios hilos, garantizando el acceso exclusivo en un momento dado por el hilo que realiza la llamada. Las dos primeras funciones incrementan o decrementan el valor de la variable en una unidad, mientras que la tercera función asigna un valor a la variable.

Empleando estas primitivas de sincronización y los conceptos comunes a todos los paquetes de hilos sobre programación multihilo, se pueden desarrollar aplicaciones con múltiples hilos de forma similar a otros paquetes.

Comparación de Win32 Threads y POSIX Threads.

Aunque el API de Win32 en relación a los hilos no se parece al API que define el estándar POSIX 1003.1c, si podemos encontrar ciertas funcionalidades aplicables a los dos API. A continuación se muestran las equivalencias entre algunas llamadas POSIX y las correspondientes en Win32 que prestan una funcionalidad similar :

POSIX THREADS
Win32 THREADS
pthread_create()
CreateThread() o _beginThreadex()
pthread_cancel()
deleteThread()
pthread_destroy()
_endThread()
pthread_join()
WaitForSingleObject()
pthread_yield()
Sleep()
pthread_mutex_init()
CreateEvent()
pthread_mutex_destroy()
CloseHandle()
pthread_mutex_lock()
WaitForSingleObject()
pthread_mutex_unlock()
ReleaseMutex()
pthread_mutex_trylock()
WaitForSingleObject()
pthread_cond_init()
CreateEvent()
pthread_cond_destroy()
CloseHandle()
pthread_cond_broadcast()
PulseEvent()
pthread_cond_signal()
PulseEvent()
pthread_cond_wait()
WaitForSingleObject()

Figura 6-4 Comparación de Win32 Threads y POSIX Threads.