Windows y los threads

Tema Threads y mensajes Versión 2.00
Resumen El presente documento busca explicar el funcionamiento de las threads en Windows y el sistema de mensajería.
Sistema Operativo Windows 9x, Me, NT, 2000, XP
Autor Gabriel Agustín Praino Fecha 30/11/2001
Búsqueda threads, mensajes
El presente material se encuentra inscripto en el Registro Nacional de Propiedad Intelectual. Todos los derechos reservados. La reproducción de la presente publicación se encuentra sujeta a los términos establecidos en la página principal de la presente obra. Prohibida la reproducción total o parcial de esta publicación en forma independiente.

Introducción

Es habitual que los programadores Windows desarrollen sus programas sin conocer realmente cómo funciona el sistema de mensajería de Windows. Aunque esto último no es un requisito indispensable para programar, su desconocimiento puede llevar a programas ineficientes o con errores.

Antes de analizar cómo funciona el mismo, es necesario presentar un nuevo concepto, las threads.

Threads

Una thread es básicamente un hilo o camino de ejecución. Es la unidad mínima de ejecución que Windows puede manejar, está asociada al proceso que la creó, y está identificada por un número entero de 32 bits.

Una thread está compuesta por:

  • Un número entero de 32 bits que la identifica unívocamente
  • El estado de los registros del procesador
  • Una pila (stack)
  • Parámetros de seguridad (Session ID y Security Descriptor)
  • Un valor de prioridad. La prioridad real de la thread es función de este valor y la prioridad del proceso a que pertenece.
  • Una entrada en la lista de ejecuciones de Windows
  • Opcionalmente, una cola de mensajes

Cada thread tiene asociado un usuario (SID: Session ID) y unos parámetros de seguridad (SD Security Descriptor), que determinan los permisos de la thread dentro del sistema operativo, y que por defecto coinciden con los de la thread principal del proceso.

Un proceso se compone se una o más threads, además del código, los datos y otros recursos del programa. Todas las threads de un proceso comparten tanto el código como los datos y se ejecutan en forma independiente. Es por ello que, cuando una o más threads deban trabajar con un mismo conjunto de datos, deberá utilizarse un sistema de semáforos, para evitar que los datos puedan verse corrompidos.

La creación de una thread se realiza utilizando la función CreateThread(). Veamos un ejemplo:

typedef struct {
...
}TDatosAplicacion;
 // Este código se ejecutará en paralelo con el resto del programa
// Cuando esta función termina, la thread termina
DWORD WINAPI ThreadProc (LPVOID lpParameter)
{
   TDatosAplicacion *pDatos = (TDatosAplicacion *)lpParameter;
   ...
   return 0;
}
 void Funcion (void)
{
   TDatosAplicacion Datos;
   DWORD dwThreadId;
   ...
    // Crear la thread
   HANDLE hThread = CreateThread (
      NULL,       // Atributo de seguridad: Usar el de la thread actual
      0,          // Tamaño del stack. Usar el tamaño por defecto
      ThreadProc, // Función inicial de la thread
      (LPVOID)&Datos,    // Datos a pasar a la thread
      0,          // Flags de creación
      &dwThreadId);
    ...
    // Esperar a que termine la thread
   DWORD dwExitCode;
   BOOL bRet;
   do {
      bRet = GetExitCodeThread (hThread, &dwExitCode);
      if (bRet && dwExitCode == STILL_ACTIVE)
       Sleep (100);
   }while (bRet && dwExitCode == STILL_ACTIVE);
    CloseHandle (hThread);
}

Al llamar el programa a la función Funcion(), el programa crea una thread, llamando a la función CreateThread(). Esta nueva thread comenzará su ejecución en la función ThreadProc(), y terminará cuando la misma retorne de esta función, o llame a la función ExitThread(). No existe ninguna forma segura de terminar una thread desde otra thread, y si bien existe la función TerminateThread() para tal fin, no se recomienda su uso.

Si se está utilizando MFC, no debe utilizarse la función CreateThread(), sino AfxBeginThread(), cuya sintaxis es similar. Esto debe hacerse, porque las MFC implementan internamente una lista de las threads que el programa está utilizando, y redefinen la función de creación de threads para poder conocer esto. El no utilizar esta función puede llevar a pérdida de memoria o a un programa inestable, si dentro de la thread se utilizan funciones de las MFC.

La función CreateThread() devuelve dos valores. El identificador de la thread o dwThreadId, que identifica a la thread, y un handle a la thread hThread, que permite realizar operaciones sobre la misma. Se podría preguntar porqué Windows utiliza un handle para realizar operaciones sobre la thread, y no directamente el Thread ID. La respuesta es que el handle a la thread no sólo identifica una thread, sino que define las operaciones que están permitidas realizar sobre la misma.

El handle devuelto debe ser cerrado cuando ya no se use, utilizando CloseHandle(), y puede ser utilizado entre otras cosas para conocer el estado de la thread.

C Runime Library y Multithreading

Muchas funciones de C no fueron pensadas para trabajar con multithreading. Por ejemplo, la función time() clásica devuelve a un puntero a una estructura estática time_t, que si fuese llamada en paralelo por varias threads de un proceso podría ser sobreescrita. Para solucionar esto, es necesario que esta librería de C en tiempo real (C run-time library o C RTL) realice algunas operaciones adicionales al crearse o liberarse una thread. Por este motivo la C-RTL proporciona sus propias funciones de creación / terminación de threads: beginthread() / exitthread().

Las MFC proporcionan también sus propias funciones de creación / terminación de threads: AfxBeginThread() y AfxExitThread().

De más está decir que la llamada a funciones de threads del API es más eficiente que sus pares C, y estas más eficientes que las de las MFC, ya que las últimas deben llamar a su vez a las anteriores.

Importante: Cualquiera de los tres grupos de funciones de threads puede utilizarse, siempre y cuando se respete la siguiente tabla.
Operación Funciones API
Ej: CreateThread()
Funciones de la librería de C. Ej: beginthread() MFC
Ej: AfxBeginThread()
Llamada a funciones API
Llamada a funciones C No
Uso de funciones/clases MFC No No

Mensajes

Antes de ver como implementa Windows el sistema de mensajería, repasemos algunos conceptos fundamentales:

  • Un programa o aplicación es visto por el sistema operativo como un proceso, identificado por un número entero, llamado process ID o PID.
  • Un proceso se compone de una o más threads, cada una de las cuales está identificada por un número entero llamado Thread ID.
  • Cada thread puede o no tener asociada una cola de mensajes.
  • Toda ventana tiene asociada una función (callback) de procesamiento de mensajes.
  • Toda ventana tiene asociada un proceso y una thread, que es la thread que la creó.
  • La función callback de la ventana es ejecutada siempre por la thread que creó la ventana.
  • Si bien no se lo explicó, se dijo que todo programa (o en realidad thread) que procese mensajes tendrá un código similar al siguiente:MSG Msg;
    while (GetMessage (&Msg, NULL, 0, 0);
    {
    TranslateMessage (&Msg);
    DispatchMessage (&Msg);
    }

Cuando un programa encola/envía un mensaje a una ventana, este mensaje es en realidad encolado/enviado a la thread de la ventana. Por eso, para que una thread pueda recibir mensajes, previamente debe crear la cola de mensajes, lo cual ocurre automáticamente cuando la misma llama por primera vez a cualquiera de las funciones de lectura de mensajes (GetMessage() en nuestro caso).

El envío de mensajes (SendMessage ()) y el encolado de mensajes (PostMessage()) es procesado de forma diferente, razón por la cual veremos estos casos por separado.

Encolado de mensajes

El manejo de mensajes se muestra esquemáticamente a continuación:

1) Cuando un programa encola un mensaje para una ventana utilizando la función PostMessage(), el mismo es insertado dentro de la cola de mensajes de la thread de la ventana, y en el campo hWnd del mensaje indica la ventana destino. Función PostMessage() retorna inmediatamente y la thread que encoló el mensaje continúa su procesamiento normal.

Existe también la función PostThreadMessage(), que trabaja en forma similar a PostMessage(), sólo que en lugar de indicarse un ventana como destino, se indica una thread. En este caso, el mensaje será encolado en forma idéntica, pero el campo hWnd tendrá un valor NULL.

2) La función GetMessage() / PeekMessage() lee y/o retira un mensaje de la cola. Los mensajes encolados por la propia thread tienen prioridad por los encolados por cualquier otra thread.

3) La función TranslateMessage(), realiza ciertas conversiones, como por ejemplo, modificar un click en el botón en un mensaje WM_CLOSE.

4) La función DispatchMessage() llama a la función callback de la ventana correspondiente, la cual procesa el mensaje y devuelve una respuesta.

5) Si la función que envió el mensaje fue PostMessage() / PostThreadMessage(), esta respuesta se descarta.

Envío de mensajes

Si una misma thread envía un mensaje (SendMessage()) para la misma thread, este mensaje es no es encolado sino que es enviado directamente a la función callback, como una llamada a una función cualquiera.

Si una thread cualquiera envía un mensaje a una ventana de otra thread, no es posible llamar directamente a la función callback de otra ventana. Si esto se hiciese así, el código de la función callback se ejecutaría bajo los parámetros de seguridad de la thread que envió el mensaje, lo cual vulneraría toda la seguridad del sistema. Por ejemplo, si Windows simplemente llamase a la función callback para informar un evento como el movimiento del mouse, esta función se ejecutaría con los parámetros de seguridad del kernel, lo cual permitiría la aplicación realizar cualquier operación.

Para evitar esto, cuando una thread envía un mensaje para una ventana de otra thread, este mensaje es encolado en forma similar a un PostMessage(), pero la thread permanece bloqueada, hasta que la thread destino procese el mensaje y devuelva su respuesta.

Problemas que pueden presentarse

El sistema de procesamiento de mensajes de Windows no es infalible, si bien generalmente se programa como si lo fuese. Entre existen dos problemas comúnmente ignorados que deben ser tenidos en cuenta:

  • Cuando una thread envía un mensaje para otra thread, la primera permanece en estado de espera hasta que la segunda procese el mensaje y devuelva una respuesta. Si la segunda thread se “colgó” o por cualquier razón no procesa mensajes, la segunda quedará esperando indefinidamente. Para evitar esto, es posible utilizar la función SendMessageTimeout(), que permite especificar el tiempo que debe esperarse la respuesta. Transcurrido este tiempo, la función retorna especificando un código de error.
  • Es común que los programadores crean que Windows garantiza el envío de los mensajes. Sin embargo esto no es así. La cola de mensajes de Windows, (como toda cola) es finita, y por ende es posible que se llene. Las funciones PostMessage() y PostThreadMessage() retornan siempre un valor indicando si el mensaje pudo o no ser encolado.

En muy raro que los programadores controlen esto en sus programas, y esta es una de las razones fundamentales por la que muchos programas fallan cuando el sistema se ve limitado de recursos o el uso del procesador es muy elevado.

Pero lamentablemente, es programáticamente muy difícil controlar estas situaciones cada vez que se envía/encola un mensaje, y es aquí donde interviene el criterio del programador. Se recomienda que estas situaciones sean controladas cuando se envía/encola un mensaje a una ventana o thread de la cual no se conoce completamente el comportamiento o el estado de la misma, como por ejemplo al enviar/encolar un mensaje para otra aplicación.

Dado que es mucho menos frecuente y más peligroso el envío de mensajes entre threads que dentro de una misma thread, también se recomienda controlar esto al enviar/encolar mensajes para una thread diferente a la actual.

La decisión de controlar estas situaciones y otros errores es una decisión que se toma en base a la experiencia, tomando en cuenta la probabilidad de falla, el ambiente donde correrá el programa, el costo de desarrollo de controlar estos errores y las implicancias de una falla.