Colorful ThreadsPrashant Shrestha, License CC-BY-2.0.

Introducción al uso de hilos en Qt

Jesús Torres
Jesús Torres
Published in
5 min readFeb 16, 2013

--

Puedes leer la última versión actualizada y corregida del artículo original en mi sitio web: jesustorres.es.

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 se utiliza la clase QThread, 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 y reimplementar el método run() insertando el código que queremos que ejecute el hilo. En este sentido el método QThread::run() es para el hilo lo que la función main() es para la aplicación.

Una vez instanciada la clase, iniciar el nuevo hilo es tan sencillo como invocar el método QThread::start().

MyThread thread;
thread.start()

El hilo terminará cuando la ejecución retorne de su método MyThread::run() o si desde el código del hilo se invocan los métodos QThread::exit() o QThread::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 se 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_1. 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 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 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 un 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 sólo 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 ultimo elemento de la cola y una serie de objetos de sincronización:

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

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:

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

Donde la instancia lock de la clase QMutexLocker sirve para evitar que el productor y el consumidor accedan al contador compartido al mismo tiempo. Concretamente:

  • El primero en crea el objeto QMutexLocker 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, continuación con su ejecución.

Usar QMutexLocker equivalente a llamar directamente a QMutex::lock() y QMutex::unlock() para obtener y liberar el cerrojo mutex. Sin embargo, es mejor utilizar QMutexLocker siempre porque reduce las posibilidades de cometer el error de olvidarnos de liberar mutex.

Por otro lado las instancias de condiciones de espera QWaitCondition permiten dormir un hilo hasta que se de una condición determinada. Como se verá más adelante, consumidor utiliza el método QWaitCondition::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::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::wait().

El consumidor

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

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

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.

Referencias

--

--

Jesús Torres
Jesús Torres

Docente e Investigador de la Universidad de La Laguna.