Esperar a que terminen los hilos

La solución que hemos usado para asegurarnos de que los hilos han terminado antes de salir no es demasiado elegante. Sólo esperamos dos segundos, y damos por supuesto que en ese tiempo los hilos han tenido tiempo de sobra para terminar, lo cual es cierto, al menos en este ejemplo. Pero puede que en otros usos de hilos esto no sea tan simple, y de todos modos, no parece que esperar un tiempo fijo antes de cerrar sea la mejor forma de terminar un programa.

Como habrás visto, para cada hilo obtenemos un manipulador, y las funciones de espera de eventos también pueden esperar a que hilos o procesos hayan terminado de ejecutarse. Podemos usar la función WaitForMultipleObjects para asegurarnos de que todos los hilos han terminado antes de salir del programa.

Para ello modificaremos un poco el programa, otra vez. En lugar de usar una variable de manipulador para cada hilo, usaremos un array de manipuladores. Para este ejemplo añadiremos dos hilos más, tendremos cuatro en total:

    HANDLE hHilo[4];
    stHora hora={70, 1, false};
    stHora hora2={1, 1, false};
    stHora hora3={70, 23, false};
    stHora hora4={1, 23, false};
...
    // Crear los hilos
    hHilo[0] = CreateThread( 0, 0, MostrarHora, (void*)&hora, 0, 0);
    hHilo[1] = CreateThread( 0, 0, MostrarHora, (void*)&hora2, 0, 0);
    hHilo[2] = CreateThread( 0, 0, MostrarHora, (void*)&hora3, 0, 0);
    hHilo[3] = CreateThread( 0, 0, MostrarHora, (void*)&hora4, 0, 0);

Cuando queramos salir, indicaremos a cada hilo que salga, mediante su variable correspondiente, esperaremos a que todos los hilos efectivamente terminen, finalmente, cerramos los manipuladores:

    hora.salir = true;
    hora2.salir = true;
    hora3.salir = true;
    hora4.salir = true;

    // Esperar a que terminen los hilos:
    WaitForMultipleObjects(4, hHilo, TRUE, INFINITE);

	// Liberar manipuladores:
    CloseHandle(hHilo[0]);
    CloseHandle(hHilo[1]);
    CloseHandle(hHilo[2]);
    CloseHandle(hHilo[3]);

Tanto este procedimiento como el anterior tienen un inconveniente: si uno de los hilos no termina, en el primer caso obtendremos un error, y en el segundo, el programa no termina. En este segundo caso podemos limitar el tiempo de espera, digamos a dos segundos, de modo que si algún hilo no termina, saldremos "por las malas".

Además, tener que cambiar una variable para salir para cada hilo tampoco parece elegante, que digamos...

Ejemplo 3

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 3 de hilos thread3.zip 2012-07-31 1629 bytes 50

Vamos a seguir afinando nuestro programa.

Usar clases

Hasta ahora hemos usado estructuras para que cada hilo acceda a un juego diferente de variables, permitiendo que la misma función pueda usarse en varios hilos simultáneamente.

Pero podemos usar clases en lugar de estructuras, con lo que no sólo tendremos un juego de variables para cada hilo, sino también las funciones para manipularlas.

Nuestra clase para mostrar la hora podría tener esta declaración:

class Hora {
    public:
        Hora(int xi, int yi) : x(xi), y(yi), seg(0), salir(false) {
            GetLocalTime(&st);
        }
        void Terminar() { salir = true; }
    private:
        void Mostrar() const;
        void EsperarCambio();
        void ActualizarSeg();
        bool Salir() const { return salir; }
        SYSTEMTIME st;  // Estructura para obtener la hora
        int x;
        int y;
        int seg;
        bool salir;
};

Pero avanzaremos un par de pasos más:

  1. Para el dato miembro salir usaremos el modificador static. Esto hará que sólo exista una instancia de este miembro para todos los objetos de la clase Hora. Para terminar todos los hilos bastará con modificar un valor, y no uno por cada instancia.
  2. Añadiremos una función miembro para el hilo. Debido a que el prototipo debe ser específicamente el que Windows espera que sea, también crearemos esa función como estática. Además, lanzaremos el hilo en el constructor de la clase Hora, por lo que necesitamos un dato miembro para almacenar el manipulador del hilo. También añadiremos un destructor, para liberar el manipulador del hilo.
class Hora {
    public:
        Hora(int xi, int yi) : x(xi), y(yi), seg(0) {
            manipulador = CreateThread( 0, 0, hiloMostrarHora, (void*)this, 0, 0);
            GetLocalTime(&st);
        }
        ~Hora() {
            WaitForSingleObject(manipulador, INFINITE);
            CloseHandle(manipulador);
        }
        void Terminar() { salir = true; }
    private:
        void Mostrar() const;
        void EsperarCambio();
        void ActualizarSeg();
        bool Salir() const { return salir; }
        SYSTEMTIME st;                    // Estructura para obtener la hora
        int x;
        int y;
        int seg;
        HANDLE manipulador;
        static bool salir;
        static DWORD WINAPI hiloMostrarHora( void* );
};

Recordemos que los datos miembro estáticos de una clase deben ser inicializados:

bool Hora::salir = false;

No voy a reproducir aquí todo el código de este ejemplo, lo puedes descargar más abajo. Sólo añadiré el código de la función del hilo y de main, para demostrar que el manejo de estos hilos se simplifica:

DWORD WINAPI Hora::hiloMostrarHora( void* par) {
    Hora *hora = (Hora*)par;  // Recuperar el parámetro, un puntero a Hora

    do {
        hora->EsperarCambio();
        hora->Mostrar();
        hora->ActualizarSeg();
    } while(!hora->Salir()); // Repetir hasta que salir sea true

    return 0;
}
...
int main() {
    HANDLE hMutex = CreateMutex(NULL, FALSE, "MutexAccesoPantalla");

    Hora hora(70, 1);
    Hora hora2(1, 1);
    Hora hora3(70, 23);
    Hora hora4(1, 23);


    // Esperar a que el usuario pulse una tecla, el hilo se sigue ejecutando:
    TrazarTexto(4,10,"Pulsa una tecla para salir");
    getchar();
    hora.Terminar();

	// Liberar manipuladores:
    CloseHandle(hMutex);
    return 0;
}

Al declarar cada objeto Hora, automáticamente se lanza el hilo correspondiente. Y para detenerlos basta con invocar al método Terminar de cualquiera de los objetos.

Ejemplo 4

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 4 de hilos thread4.zip 2012-07-31 1629 bytes 43

Elaborando una jerarquía de clases

Hasta ahora todos los hilos eran del mismo tipo, todos mostraban la hora, pero ¿qué pasaría si queremos añadir otros hilos con acceso a la pantalla para otras tareas?

Por ejemplo, crearemos un segundo tipo de hilo para mostrar textos rotativos.

La idea es proporcionar un texto de una longitud l y que se muestre a partir de las coordenadas (x,y) una cantidad de caracteres n. Cada cierto tiempo el texto se debe desplazar un carácter a la izquierda, en la misma posición de pantalla, de modo que un carácter desaparece por la izquierda y se añadirá un carácter a la derecha. Cuando no queden caracteres para añadir, se empezará otra vez desde el comienzo de la cadena. Para evitar errores, n debe ser mayor que l.

Para rotar el texto podríamos usar la función ScrollConsoleScreenBuffer, ya que es mucho más rápido rotar parte de la pantalla que repintar el texto completo cada vez. Esta función, además, no tiene en cuenta la posición del cursor, ni la modifica, de modo que no interfiere con otras funciones de impresión de caracteres.

Sin embargo, aunque impementaremos una función Rotar no la usaremos en el ejemplo, ya que pretendo ilustrar el conflicto al intentar imprimir desde varios hilos a la vez. :)

Además, la función que habíamos diseñado para mostrar un texto no nos sirve para esta tarea, ya que debemos ser capaces de mostrar sólo parte de una cadena, es decir, necesitamos una función para imprimir subcadenas. Esa función puede ser WriteConsole o WriteConsoleOutput.

Partiremos de nuestro último ejemplo, y añadiremos una segunda clase.

class TextoRotativo {
    public:
        TextoRotativo(int xc, int yc, int anchoc, int retc, const char *cad);
        ~TextoRotativo();
        void Terminar() { salir = true; }

    private:
        void Mostrar();
        void IncrementaPos();
        void Rotar();
        void Esperar() { Sleep(retardo); }
        bool Salir() const { return salir; }
        bool Reservar();
        void Liberar();
        char *cadena;
        int longitud;
        int pos;
        int x;
        int y;
        int ancho;
        int retardo;
        HANDLE manipulador;
        HANDLE hMutex;
        static bool salir;
        static DWORD WINAPI hiloRotarTexto( void* par );
};

A continuación se muestra una posible implementación de la clase TextoRotativo. No usaremos la función Rotar() en este ejemplo, pero se añade la implementación para completar el ejemplo.

TextoRotativo::TextoRotativo(int xc, int yc, int anchoc, int retc, const char *cad) :
    pos(0), x(xc), y(yc), ancho(anchoc), retardo(retc) {
    hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, "MutexAccesoPantalla");
    manipulador = CreateThread( 0, 0, hiloRotarTexto, (void*)this, 0, 0);
    longitud = strlen(cad);
    cadena = new char[longitud+1];
    strcpy(cadena, cad);
}

TextoRotativo::~TextoRotativo() {
    WaitForSingleObject(manipulador, INFINITE);
    CloseHandle(manipulador);
    CloseHandle(hMutex);
    delete[] cadena;
}

void TextoRotativo::Rotar() {
    SMALL_RECT re1 = {x+1, y, x+ancho, y};
    COORD coor = {x, y};
    CHAR_INFO ci = { {(pos+ancho) >= longitud ? cadena[pos+ancho-longitud] : cadena[pos+ancho]},
        FOREGROUND_BLUE+FOREGROUND_GREEN+FOREGROUND_RED};

    ScrollConsoleScreenBuffer(GetStdHandle(STD_OUTPUT_HANDLE), &re1, 0, coor, &ci);
}

bool TextoRotativo::Reservar() {
    if(WaitForSingleObject(hMutex, INFINITE) == WAIT_OBJECT_0) return true;
    return false;
}

void TextoRotativo::Liberar() {
    ReleaseMutex(hMutex);
}

void TextoRotativo::Mostrar() {
    COORD dwPos = {x, y};
    int n = 0;
    int m;

    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), dwPos);
    n = ancho < longitud-pos ? ancho : longitud-pos;
    WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), cadena+pos, n, 0, 0);
    if(ancho-n > 0)
       WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), cadena, ancho-n, 0, 0);
}

void TextoRotativo::IncrementaPos() {
    pos++;
    if(!cadena[pos]) pos = 0;
}

Añadimos la función del hilo:

DWORD WINAPI TextoRotativo::hiloRotarTexto( void* par) {
    TextoRotativo *texto = (TextoRotativo*)par;

    // Texto inicial
    if(texto->Reservar()) {
        texto->Mostrar();
        texto->Liberar();
    }
    do {
        // Pintar 'ancho' caracteres a partir de (x,y), empezando en la
        // posición 'pos' de la 'cadena':
        if(texto->Reservar()) {
            texto->Mostrar();
            texto->Liberar();
            texto->IncrementaPos();
            texto->Esperar();
        }
    } while(!texto->Salir());
    return 0;
}

Definimos el miembro estático salir:

bool TextoRotativo::salir = false;

Y creamos algunos objetos en la función main:

int main() {
    HANDLE hMutex = CreateMutex(NULL, FALSE, "MutexAccesoPantalla");
    SetConsoleOutputCP(1252);

    Hora hora(70, 1);
    Hora hora2(1, 1);
    Hora hora3(70, 23);
    Hora hora4(1, 23);
    TextoRotativo texto(20,2,15,100, "Texto rotativo de prueba ------ ");
    TextoRotativo texto2(20, 15, 10, 50, "Articulos Con Clase - Salvador Pozo - ");
    TextoRotativo texto3(5, 20, 70, 60, "Generalmente tendemos a pensar que la multitarea se limita a poder tener abiertas varias ventanas "
        "con distintas aplicaciones a la vez, pero hay mucho más. Por ejemplo, un navegador web, puede "
        "tener abiertas varias páginas a la vez. A su vez, cada página, mientras se carga, puede estar descargando "
        "varios recursos: texto, imágenes, etc, y actualizando la pantalla a medida que los descarga. ------------------ ");
    TextoRotativo texto4(30, 0, 20, 150, "Pulsa una tecla para salir del programa.    ----   ");

    // Esperar a que el usuario pulse una tecla, el hilo se sigue ejecutando:
    getchar();
    hora.Terminar();
    texto.Terminar();

	// Liberar manipuladores:
    CloseHandle(hMutex);
    return 0;
}

Ejemplo 5

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 5 de hilos thread5.zip 2012-07-31 2703 bytes 44

Clase base para pantalla

Pero con esta implementación vuelven a aparecer tareas y datos repetidos. Personalmente, no me gusta tener dos variables salir, lo que implica invocar dos veces al método Terminar.

Una solución para simplificar estas clases es crear una jerarquía, creando una clase base, y derivando Hora y RotarTexto de esa clase.

Crearemos una clase virtual pura para encapsular todos los datos y funciones relacionados directamente con la gestión del hilo y el mutex:

// Clase virtual para crear hilos con acceso a pantalla:
class HiloPantalla {
    public:
        HiloPantalla( LPTHREAD_START_ROUTINE hilo ) {
            manipulador = CreateThread( 0, 0, hilo, (void*)this, 0, 0);
            hMutex = CreateMutex(NULL, FALSE, "MutexAccesoPantalla");
        }
        ~HiloPantalla() {
            CloseHandle(manipulador);
            CloseHandle(hMutex);
        }
        void Terminar() {
            salir = true;
            WaitForSingleObject(manipulador, INFINITE);
        }
    protected:
        virtual void Mostrar() const  = 0;
        bool Reservar() {
            if(WaitForSingleObject(HiloPantalla::hMutex, INFINITE) == WAIT_FAILED) return false;
            return true;
        }
        void Liberar() {
            ReleaseMutex(HiloPantalla::hMutex);
        }
        bool Salir() const { return salir; }
        static bool salir;
        HANDLE hMutex;
        HANDLE manipulador;
};

bool HiloPantalla::salir = false;

Esto descarga a las clases derivadas que encapsulen hilos con acceso a pantalla de todas las funciones y datos repetitivos, dejando sólo aquellos que afectan a la tarea concreta que realiza cada hilo. La función Mostrar está declarada como virtual pura, esto nos obliga a definirla en las clases derivadas e impide que se puedan crear objetos de la clase HiloPantalla.

Además, dado que sólo hemos declarado como públicos el constructor, el destructor y el método Terminar, el usuario que use estas clases para definir hilos no tendrá acceso a ningún otro método. Tan pronto como el hilo sea creado se empezará a ejecutar, y cuando el objeto se destruya, se detendrá.

Las clases derivadas para mostrar la hora y los textos rotativos tendrán estas declaraciones:

class Hora : public HiloPantalla {
    public:
        Hora(int xi, int yi) : HiloPantalla(hiloMostrarHora), x(xi), y(yi), seg(0) {
            GetLocalTime(&st);
        }
    private:
        void Mostrar() const;
        void EsperarCambio();
        void ActualizarSeg();
        SYSTEMTIME st;                    // Estructura para obtener la hora
        int x;
        int y;
        int seg;
        static DWORD WINAPI hiloMostrarHora( void* );
};

class TextoRotativo : public HiloPantalla {
    public:
        TextoRotativo(int xc, int yc, int anchoc, int retc, const char *cad):
           HiloPantalla(hiloRotarTexto), pos(0), x(xc), y(yc), ancho(anchoc), retardo(retc) {
            longitud = std::strlen(cad);
            // Para que sea posible mostrar textos más cortos que la anchura del mensaje rotativo
            // hay dos posibilidades:
            // 1) Añadir caracteres hasta que longitud==ancho
            // 2) Repetir el mensaje hasta que longitud>=ancho
            // Optamos por la 1ª forma
            if(longitud < ancho) longitud = ancho;
            cadena = new char[longitud+1];
            std::strcpy(cadena, cad);
            while((int)std::strlen(cadena) < longitud) std::strcat(cadena, "-");
        }
        ~TextoRotativo() {
            delete[] cadena;
        }

    private:
        void Mostrar() const;
        void IncrementaPos();
        void Rotar();
        void Esperar() { Sleep(retardo); }
        char *cadena;
        int longitud;
        int pos;
        int x;
        int y;
        int ancho;
        int retardo;
        static DWORD WINAPI hiloRotarTexto( void* par );
};

De nuevo, en las clases derivadas no hemos declarado métodos públicos, salvo los constructores y destructores. Es decir, el programador no tendrá ningún control sobre los procesos de cada hilo, una vez estos sean creados.

Como el código empieza a ser un poco largo, dividiremos este proyecto en varios ficheros:

  • Un fichero de cabecera para la clase base virtual HiloPantalla.
  • Un fichero de cabecera para cada clase derivada.
  • Un fichero de definición para cada clase derivada.
  • Un fichero con el programa principal, y la función main.

Ejemplo 6

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 6 de hilos thread6.zip 2012-07-31 4753 bytes 47

Ventajas de disponer de una jerarquía de clases

Ahora que tenemos una estructura de programa bien diseñada, tenemos la ventaja de que es relativamente sencillo añadir nuevas clases para nuevas funcionalizades.

Por ejemplo, añadiremos una clase para implementar animaciones ASCII. Por ejemplo, para hecer un "molinillo" de cuatro fotogramas de 3x3 casillas usaremos estos gráficos:

{salida} | / \ | / --- \ | / \ {finsalida}

La idea es sencilla, crearemos una clase que tomará como valores para el constructor las coordenadas donde se mostrará la animación, sus dimensiones de ancho y alto, el número de fotogramas o cuadros, el retardo entre imágenes y una cadena con el texto de la animación. En la cadena empaquetaremos toda la animación completa, poniendo para cada fotograma todas las filas, una a continuación de otra, y todos los fotogramas seguidos. Esto dará como resultado una cadena de (ancho*alto*cuadros) caracteres.

De nuevo añadiremos dos ficheros, uno para la declaración de la clase y otro para la implementación.

La declaración de la clase puede tener esta forma:

// Clase que usa un hilo para mostrar una animación en las coordenadas indicadas:
// V 1.0 (C) Julio 2012 Salvador Pozo : Con Clase : http://conclase.net

#ifndef __HILOANIM_H__
#define __HILOANIM_H__

#include <cstring>
#include "hilopantalla.h"

class Animacion : public HiloPantalla {
    public:
        Animacion(int xi, int yi, int f, int c, int d, int ret, const char *a) :
            HiloPantalla(hiloAnimacion), cuadroactual(0), x(xi), y(yi), filas(f), cols(c), retardo(ret), cuadros(d)
        {
            int l = strlen(a)+1;
            anim = new char[l];
            std::strcpy(anim, a);
        }
        ~Animacion() {
            delete[] anim;
        }
    private:
        void Mostrar() const;
        void Esperar() { Sleep(retardo); }
        void SiguienteCuadro() {
            cuadroactual++;
            if(cuadroactual >= cuadros) cuadroactual = 0;
        }
        char *anim;
        int cuadroactual;
        int x;
        int y;
        int filas;
        int cols;
        int retardo;
        int cuadros;
        static DWORD WINAPI hiloAnimacion( void* );
};
#endif

La implementación es también sencilla:

// Implementación de la clase que usa un hilo para mostrar una animación:
// V 1.0 (C) Julio 2012 Salvador Pozo : Con Clase : http://conclase.net

#include <cstdio>
#include "hiloanim.h"

void Animacion::Mostrar() const {
    COORD dwPos = {x, y};

    for(int i = 0; i < filas; i++) {
        SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), dwPos);
        WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), &anim[i*cols+cuadroactual*cols*filas], cols, 0, 0);
        dwPos.Y++;
    }
}

DWORD WINAPI Animacion::hiloAnimacion( void* par) {
    Animacion *anima = (Animacion*)par;

    do {
        if(anima->Reservar()) {
            anima->Mostrar();
            anima->Liberar();
            anima->SiguienteCuadro();
            anima->Esperar();
        }
    } while(!anima->Salir());
    return 0;
}

En el programa principal incluimos el fichero de cabecera, y en la función main creamos los objetos Animacion que necesitemos:

#include "hiloanim.h"
...
int main() {
...
    Animacion animacion2(6, 8, 1, 1, 4, 100, "|/-\\"); // Escapar los caracteres \
...
}

Ejemplo 7

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 7 de hilos thread7.zip 2012-07-31 6200 bytes 50

Usar un hilo para leer el teclado

Pero actualizar la pantalla no es la única utilidad de los hilos. La lectura del teclado es otro posible uso. El teclado es un dispositivo de entrada asíncrono, y por lo tanto, a ser su estado impredecible, su lectura es una tarea complicada para un programa secuencial. Usar un hilo nos facilita las cosas, ya que éste es capaz de actualizar el estado de un objeto en segundo plano, y nuestro programa leer el estado del teclado sin preocuparse por si una espera bloquea la ejecución.

Usando el API de Windows para leer el teclado disponemos de dos funciones que nos pueden servir:

  • GetKeyState: esta función es síncrona, es decir, verifica el estado de una tecla en el momento en que es invocada. Si la tecla está pulsada, el bit de mayor peso del valor de retorno estará puesto a 1. Como el valor de retorno es un SHORT, tendrá 16 bits. Tenemos que enmascarar el valor de retorno con 0x8000.
  • GetAsyncKeyState: esta función es asíncrona, es decir, devuelve el estado de una tecla desde la última vez que esta misma función fue invocada. Esto nos permite saber si una tecla fue pulsada, aunque no hayamos estado verificándolo en todo momento. También se activa el bit de mayor peso si la tecla fue pulsada.

Crearemos un hilo para verificar el estado de las teclas del cursor y de la tecla de escape:

struct stDirecciones {
    bool arriba;
    bool abajo;
    bool izquierda;
    bool derecha;
    bool escape;
};

DWORD WINAPI LeeTeclado( void* par) {
    stDirecciones *dirs = (stDirecciones*)par;

    do {
        dirs->arriba    = GetKeyState(VK_UP) & 0x8000;
        dirs->abajo     = GetKeyState(VK_DOWN) & 0x8000;
        dirs->izquierda = GetKeyState(VK_LEFT) & 0x8000;
        dirs->derecha   = GetKeyState(VK_RIGHT) & 0x8000;
        dirs->escape    = GetKeyState(VK_ESCAPE) & 0x8000;
    } while (!dirs->escape);

    return 0;
}

Para lanzarlo, haremos como en los ejemplos anteriores:

int main() {

    HANDLE hTeclado;
    stDirecciones dirs;

    hTeclado = CreateThread( 0, 0, LeeTeclado, (void*)&dirs, 0, 0);
...
    CloseHandle(hTeclado);
    return 0;
}

En cualquier momento de la ejecución del programa, entre la creación y destrucción del hilo, podemos verificar el estado del teclado consultado la estructura dirs. Una ventaja evidente es que podemos detectar la pulsación simultánea de varias teclas, y movernos en diagonal, por ejemplo.

Nota: Por supuesto, esto es sólo un ejemplo de demostración. Podríamos haber usado las funciones de lectura asíncronas, de modo que el hilo es innecesario.

Ejemplo 8

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 8 de hilos thread8.zip 2012-07-31 1208 bytes 48

Juego de demostración

Por último añadiré un pequeño juego, muy básico, para ilustrar el uso de hilos.

Ejemplo 9

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 9 de hilos thread9.zip 2012-07-31 3827 bytes 48