# Introducción al uso de hilos en Qt

Debido a la existencia del bucle de mensajes, no se pueden ejecutar tareas de larga duración en los *slots*. Si lo hiciéramos, la ejecución tardaría en volver al bucle de mensajes, retrasando el momento en el que la aplicación puede procesar nuevos eventos de los usuarios.

Por eso lo habitual es que desde los *slots* se deleguen esas tareas a hilos de trabajo —o *worker thread*— de tal manera que se ejecuten mientras el hilo principal sigue procesando los eventos que lleguen a la aplicación.

# Gestionar hilos con Qt

Para usar hilos en [Qt](/proyecto-qt-framework-de-desarrollo-de-aplicaciones/) se utiliza la clase [QThread](https://doc.qt.io/qt-5/qthread.html), donde cada instancia de dicha clase representa a un hilo de la aplicación.

Crear un hilo es tan sencillo como heredar la clase [QThread](https://doc.qt.io/qt-5/qthread.html) y reimplementar el método [run](https://doc.qt.io/qt-5/qthread.html#run)() insertando el código que queremos que ejecute el hilo. En este sentido, el método [QThread](https://doc.qt.io/qt-5/qthread.html)::[run](https://doc.qt.io/qt-5/qthread.html#run)() es para el hilo, lo que la función `main()` es para la aplicación.

```c++
class MyThread : public QThread
{
    Q_OBJECT

protected:
    void run();
};

void MyThread::run()
{
    // Aquí el código a ejecutar en el hilo...
}
```

Una vez instanciada la clase, iniciar el nuevo hilo es tan sencillo como invocar el método [QThread](https://doc.qt.io/qt-5/qthread.html)::[start](https://doc.qt.io/qt-5/qthread.html#start)().

```plaintext
MyThread thread;
thread.start()
```

El hilo terminará cuando la ejecución retorne de su método MyThread::[run](https://doc.qt.io/qt-5/qthread.html#run)() o si desde el código del hilo se invocan los métodos [QThread](https://doc.qt.io/qt-5/qthread.html)::[exit](https://doc.qt.io/qt-5/qthread.html#exit)() o [QThread](https://doc.qt.io/qt-5/qthread.html)::[quit](https://doc.qt.io/qt-5/qthread.html#quit)().

# Problema del buffer finito

Generalmente, los hilos no se crean directamente en los *slots* en los que son necesarios, sino en la función `main()`, en el constructor de la clase de la ventana que los va a utilizar o en otros sitios similares. Eso es así por una cuestión de eficiencia, ya que crear y destruir hilos según cuando son necesarios tiene cierto coste.

La única cuestión es que entonces un *slot* debe poder entregar la tarea al hilo correspondiente que ha sido creado previamente. Como todos los hilos comparten la memoria del proceso, esto no debe ser un problema, pero realmente entraña ciertas dificultades relacionadas con la concurrencia.

Para ilustrarlo supongamos que hemos abierto un archivo de vídeo para procesarlo y que un *slot* de la clase de la ventana es invocado cada vez que se dispone de un nuevo *frame*. De hecho, un ejemplo de cómo usar de esta manera [QMovie](https://doc.qt.io/qt-5/qmovie.html) se trata en el artículo [Como usar QMovie en Qt](/como-usar-qmovie-en-qt).

La función del *slot* sería la de transferir al hilo el *frame* para que se haga cargo de su procesamiento. Teniendo esto en cuenta, el problema al que nos enfrentamos podría ser descrito de la siguiente manera:

* El *slot* obtiene los *frames*, por lo que sería nuestro *productor*. Como se ejecuta desde el bucle de mensajes, sabemos que siempre lo hace dentro del hilo principal del proceso.
    
* El hilo de trabajo encargado del procesamiento sería nuestro *consumidor*, ya que toma los *frames* entregados por el productor.
    
* Ambos comparten un *buffer* de *frames* de tamaño fijo que se usa a modo de cola circular. El *productor* insertaría los *frames* en la cola mientras el *consumidor* los extraería.
    
* No será un problema que el *productor* añada más *frames* de los que caben en la cola porque la cola será circular. Es decir, aunque se llene, se siguen añadiendo *frames* sobrescribiendo los más antiguos. Es preferible perder *frames* a hacer crecer la cola, retrasando cada vez más el procesamiento de los nuevos *frames*, hasta quedarnos sin memoria. Para que esto funcionen *productor* y *consumidor* tendrán que compartir las posiciones del primer y último elemento de la cola.
    
* Si habrá que controlar que el *consumidor* no intente extraer más *frames* cuando ya no queden.
    

Para que todo esto funcione correctamente vamos a necesitar una serie de elementos de sincronización que ayuden a ambos hilos a coordinarse:

* Un cerrojo —o *mutex*— de exclusión mutua [QMutex](https://doc.qt.io/qt-5/qmutex.html) que serialice la ejecución del código en ambos hilos que manipulan la cola y su contador. La idea es que mientras uno de los hilos esté manipulando la cola, el otro tenga que esperar.
    
* Una condición de espera [QWaitCondition](https://doc.qt.io/qt-5/qwaitcondition.html) para que el *consumidor* pueda dormir mientras la cola esté vacía. La siguiente vez que el productor inserte un *frame* en la cola, utilizaría la condición de espera para notificar al consumidor que puede volver a extraerlos.
    

Teniendo todo esto presente, a continuación desarrollamos una posible solución.

## La clase FiniteBuffer

Vamos a encapsular el *buffer* compartido dentro de una clase propia, de tal forma que el acceso al mismo solo pueda realizarse usando los métodos seguros que implementaremos.

* `void insertFrame(const QImage& frame)` Insertar la imagen `frame` en el buffer de *frames*.
    
* `QImage extractFrame()` Extraer el *frame* más antiguo del *buffer*.
    

Como ya hemos comentado, los hilos deben compartir: la cola, las posiciones del primer y último elemento de la cola y una serie de objetos de sincronización:

```c++
class FiniteBuffer : public QObject
{
    Q_OBJECT

public:
    FiniteBuffer(int size);
    ~FiniteBuffer();

    // Métodos de inserción y extracción para el productor y el
    // consumidor, respectivamente.
    void insertFrame(const QImage& frame);
    QImage extractFrame();

private:
    /* La cola de frames se puede construir con un array de C:
        const int bufferSize_;    // Tamaño de la cola
        QImage[] buffer_;         // Cola de frames como array de C
       pero es más cómodo y menos propenso a errores usar QVector,
       std::vector o estructuras de datos similares de C++ o Qt.
    */
    QVector<QImage> buffer_;    // Cola de frames
    int bufferTail_;        // Posición del último frame insertado
    int bufferHead_;        // Posición del último frame extraído

    // Objetos de sincronización
    QWaitCondition bufferNotEmpty_;
    QMutex mutex_;
}
```

que debemos inicializar adecuadamente en el constructor de nuestra nueva clase:

```plaintext
FiniteBuffer::FiniteBuffer(int size)
    : buffer_(size), numUsedBufferItems_(0),
      bufferHead_(-1), bufferTail_(-1)
{}
```

## El productor

El código en el *slot* de la ventana principal llamado cada vez que se dispone de un nuevo *frame* podría tener el siguiente aspecto:

```c++
void MyWindow::on_video_updated(const QRect& rect)
{
    finiteBuffer_->insertFrame(movie_->currentImage());
}
```

siendo el método `FiniteBuffer::insertFrame()` el siguiente:

```c++
void FiniteBuffer::insertFrame(const QImage& frame)
{
    // Bloquear el cerrojo. Es lo mismo que hacer manualmente:
    // mutex_.lock()
    QMutexLocker lock(&mutex);
  
    // El código del productor a partir de este punto no se
    // ejecutará si el consumidor ha bloqueado el cerrojo
    // primero.
  
    // Insertar el frame en la cola
    buffer_[++bufferTail_ % buffer_.size()] = frame;
    bufferNotEmpty_.wakeAll();      // Despertar al consumidor si
                                    // esperaba por más frames.
  
    // El cerrojo se libera automáticamente al salir de la función
    // y destruirse lock. Es lo mismo que hacer manualmente:
    // mutex_.unlock()
}
```

Donde la instancia `lock` de la clase [QMutexLocker](http://doc.qt.io/qt-5/qmutexlocker.html) sirve para evitar que el *productor* y el *consumidor* accedan al contador compartido al mismo tiempo. Concretamente:

* El primero crea el objeto [QMutexLocker](http://doc.qt.io/qt-5/qmutexlocker.html) obtiene el cerrojo `mutex`. Si un segundo hilo llega a ese método mientras el otro tiene el cerrojo, simplemente se duerme a la espera de que el cerrojo sea liberado por el primero.
    
* El salir del método se libera el cerrojo `mutex`. En ese momento uno de los hilos que espera obtener el cerrojo se despierta y lo obtiene, para continur con su ejecución.
    

Usar [QMutexLocker](http://doc.qt.io/qt-5/qmutexlocker.html) equivalente a llamar directamente a [QMutex](https://doc.qt.io/qt-5/qmutex.html)::[lock](https://doc.qt.io/qt-5/qmutex.html#lock)() y [QMutex](https://doc.qt.io/qt-5/qmutex.html)::[unlock](https://doc.qt.io/qt-5/qmutex.html#unlock)() para obtener y liberar el cerrojo `mutex`. Sin embargo, es mejor utilizar [QMutexLocker](http://doc.qt.io/qt-5/qmutexlocker.html) siempre, porque reduce las posibilidades de cometer el error de olvidarnos de liberar `mutex`.

Por otro lado, las instancias de condiciones de espera [QWaitCondition](https://doc.qt.io/qt-5/qwaitcondition.html) permiten dormir un hilo hasta que se dé una condición determinada. Como se verá más adelante, *consumidor* utiliza el método [QWaitCondition](https://doc.qt.io/qt-5/qwaitcondition.html)::[wait](https://doc.qt.io/qt-5/qwaitcondition.html#wait)() para dormir si la cola está vacía. Antes de hacerlo, libera temporalmente el cerrojo `mutex_`, permitiendo que el *productor* se pueda ejecutar en el código que protege.

El *productor* utiliza el método [QWaitCondition](https://doc.qt.io/qt-5/qwaitcondition.html)::[weakAll](https://doc.qt.io/qt-5/qwaitcondition.html#weakAll)() después de insertar un elemento con el objeto de despertar al consumidor. Obviamente, este deberá bloquear el cerrojo `mutex_` antes de volver del método [QWaitCondition](https://doc.qt.io/qt-5/qwaitcondition.html)::[wait](https://doc.qt.io/qt-5/qwaitcondition.html#wait)().

## El consumidor

El código del hilo consumidor podría tener el siguiente aspecto:

```c++
class FrameProcessingThread : public QThread
{
    Q_OBJECT

public:
    FrameProcessingThread(FiniteBuffer* buffer,
                          QObject *parent = 0)
        : QThread(parent), buffer_(buffer)
    {}

    void run()
    {
        while(true) {
            QImage image = buffer_->extractFrame();

            //
            // Aquí va el código para procesar cada frame...
            //
            // ...
            //
        }
    }

private:
    FiniteBuffer* buffer_;
};
```

donde el código del método `FiniteBuffer::removeFrame()` es muy similar al de inserción:

```c++
QImage FiniteBuffer::extractFrame()
{
    // Bloquear el cerrojo. Es lo mismo que hacer manualmente:
    // mutex_.lock()
    QMutexLocker lock(&mutex);

    // El código del productor a partir de este punto no se
    // ejecutará si el productor ha bloqueado el cerrojo
    // primero.
  
    if (bufferHead_ == bufferTail_)     // ¿Cola vacía?...
        bufferNotEmpty_.wait(&mutex);   // Dormir si es así
    }
    QImage image = buffer_[++bufferHead_ % buffer_.size()];

    // El cerrojo se libera automáticamente al salir de la función
    // y destruirse lock. Es lo mismo que hacer manualmente:
    // mutex_.unlock()  
    return image;
}
```

## El constructor de la ventana principal

Finalmente, es en constructor de ventana principal del programa `MyWindow` donde debe crearse el buffer `FiniteBuffer` y el hilo encargado del procesamiento de los *frames*. Es decir, nuestro consumidor.

```c++
MyWindow::MyWindow(QWidget *parent)
{
    // ...
  
    finiteBuffer_ = new FiniteBuffer(20);
    FrameProcessingThread frameProcessingThread(finiteBuffer_);
    frameProcessingThread.start();
  
    // ...
}
```

# Referencias

* [Como usar QMovie en Qt](/como-usar-qmovie-en-qt)
    
* [Starting Threads with QThread](http://doc.qt.io/qt-4.8/threads-starting.html) — Qt Documentation.
    
* [Wait Conditions Example](http://doc.qt.io/qt-5/qtcore-threads-waitconditions-example.html) — Qt Documentation.
    
* [Producer-consumer problem](http://en.wikipedia.org/wiki/Producer-consumer_problem) — Wikipedia.
