El ejemplo Mandelbrot, demuestra la programación concurrente usando el Framework Qt, específicamente explica cómo usar un hilo trabajador para realizar operaciones computacionales complejas sin bloquear el hilo principal del ciclo de eventos [1].
El cómputo pesado en el presente ejemplo es el conjunto Mandelbrot, probablemente el fractal más famoso del mundo.
En la vida real, el enfoque aquí descrito es aplicable a una amplia gama de problemas, incluyendo la red de E / S síncrona y acceso a base de datos, donde la interfaz de usuario debe ser capaz de responder mientras una operación pesada está teniendo lugar.
La aplicación Mandelbrot admite zoom y el scroll con el ratón o el teclado. Para evitar la congelación de bucle de eventos del hilo principal (y, en consecuencia, la interfaz de usuario de la aplicación), ponemos todo el cálculo fractal en un subproceso de trabajo independiente. El hilo emite una señal cuando se termina de hacer la representación del fractal.
Durante el tiempo en que el subproceso de trabajo esta recalculando el fractal para reflejar la nueva posición factor de zoom, el hilo principal, simplemente escala el pixmap previamente representado para proporcionar una respuesta inmediata. El resultado no se ve tan bueno como lo que el subproceso de trabajo con el tiempo termina dando, pero al menos hace que la aplicación sea más sensible. En la secuencia de imágenes que a continuación mostramos se ve: la imagen original, la imagen escalada, y la imagen representada nuevamente.
Del mismo modo, cuando el usuario se desplaza, el mapa de pixeles anterior se desplaza inmediatamente, revelando las áreas no pintadas más allá del borde del mapa de píxeles, mientras que la imagen se representa por el subproceso de trabajo.
La aplicación consta de dos clases:
· RenderThread es una subclase QThread que representa (render) el conjunto de Mandelbrot.
· MandelbrotWidget es una subclase QWidget que muestra el conjunto de Mandelbrot en la pantalla y permite al usuario las operaciones de ampliar (zoom) y desplazar (scroll).
Definición de la clase RenderThread
Vamos a empezar con la definición de la clase RenderThread:
class RenderThread : public QThread
{
Q_OBJECT
public:
RenderThread(QObject *parent = 0);
~RenderThread();
void render(double centerX, double centerY, double scaleFactor, QSize resultSize);
signals:
void renderedImage(const QImage &image, double scaleFactor);
protected:
void run();
private:
uint rgbFromWaveLength(double wave);
QMutex mutex;
QWaitCondition condition;
double centerX;
double centerY;
double scaleFactor;
QSize resultSize;
bool restart;
bool abort;
enum { ColormapSize = 512 };
uint colormap[ColormapSize];
};
La clase hereda de QThread de esa forma adquiere la capacidad de ejecutarse en un subproceso independiente. Además del constructor y destructor, render () es la única función pública. Cada vez que el hilo realiza la representación de una imagen, emite la señal renderedImage.
La función protegida run () se reimplementada de QThread. Se llama automáticamente cuando se inicia el hilo.
En la sección privada, tenemos un QMutex, un QWaitCondition, y algunos otros miembros de datos. La exclusión mutua protege al otro miembro de datos.
Implementación de la clase RenderThread
RenderThread::RenderThread(QObject *parent)
: QThread(parent)
{
restart = false;
abort = false;
for (int i = 0; i < ColormapSize; ++i)
colormap[i] = rgbFromWaveLength(380.0 + (i * 400.0 / ColormapSize));
}
En el constructor, inicializamos las operaciones de reiniciar y abortar las variables en false. Estas variables controlan el flujo de la función run (). También se inicia arreglo de mapa de colores, que contiene una serie de colores RGB
RenderThread::~RenderThread()
{
mutex.lock();
abort = true;
condition.wakeOne();
mutex.unlock();
wait();
}
El destructor se puede llamar en cualquier momento mientras el hilo está activo. Fijamos abortar en true para decirle a run () que debe detener la ejecución tan pronto como sea posible. También hacemos una llamada a QWaitCondition::wakeOne() para despertar el hilo si está durmiendo. (Como veremos cuando examinemos run (), el hilo es puesto a dormir cuando no tiene nada que hacer.)
Lo importante a notar aquí es que run () se ejecuta en su propio hilo (el subproceso de trabajo), mientras que el constructor y el destructor RenderThread (así como la función render ()) son llamados por el subproceso que creó el subproceso de trabajo. Por lo tanto, necesitamos un mutex para proteger los accesos a las variables de abortar y condición, que pueden ser leídas en cualquier momento el método run ().
Al final del destructor, que llamamos QThread::wait() para esperar a que el método run () ha salido antes de invocar el destructor de la clase base.
void RenderThread::render(double centerX, double centerY, double scaleFactor,
QSize resultSize)
{
QMutexLocker locker(&mutex);
this->centerX = centerX;
this->centerY = centerY;
this->scaleFactor = scaleFactor;
this->resultSize = resultSize;
if (!isRunning()) {
start(LowPriority);
} else {
restart = true;
condition.wakeOne();
}
}
La función render () es llamada por el MandelbrotWidget cada vez que necesita generar una nueva imagen del conjunto de Mandelbrot. Los parámetros CenterX, CenterY y scaleFactor especifican la parte del fractal para representar; resultSize especifica el tamaño de la QImage resultante.
La función almacena los parámetros de las variables miembro. Si el hilo no se está ejecutando, se inicia; de otro modo, se establece el reinicio a true (diciendo al run () que debe detener cualquier cálculo sin terminar y empezar de nuevo con los nuevos parámetros) y se despierta el hilo, que podría estar durmiendo.
void RenderThread::run()
{
forever {
mutex.lock();
QSize resultSize = this->resultSize;
double scaleFactor = this->scaleFactor;
double centerX = this->centerX;
double centerY = this->centerY;
mutex.unlock();
La función run() es muy grande, por lo cual se divide en dos partes. El cuerpo de la función es un bucle infinito que se inicia mediante el almacenamiento de los parámetros de representación en variables locales. Como de costumbre, protegemos los accesos a las variables miembro utilizando el mutex de la clase. El almacenamiento de las variables miembro en variables locales nos permite minimizar la cantidad que de código que necesita ser protegido por un mutex. Esto asegura que el hilo principal nunca tendrá que bloquear durante demasiado tiempo cuando se necesita acceder a las variables miembros de RenderThread (por ejemplo, en el render ()). La palabra clave forever, es como foreach, una seudo-palabra clave de Qt.
int halfWidth = resultSize.width() / 2;
int halfHeight = resultSize.height() / 2;
QImage image(resultSize, QImage::Format_RGB32);
const int NumPasses = 8;
int pass = 0;
while (pass < NumPasses) {
const int MaxIterations = (1 << (2 * pass + 6)) + 32;
const int Limit = 4;
bool allBlack = true;
for (int y = -halfHeight; y < halfHeight; ++y) {
if (restart)
break;
if (abort)
return;
uint *scanLine =
reinterpret_cast<uint *>(image.scanLine(y + halfHeight));
double ay = centerY + (y * scaleFactor);
for (int x = -halfWidth; x < halfWidth; ++x) {
double ax = centerX + (x * scaleFactor);
double a1 = ax;
double b1 = ay;
int numIterations = 0;
do {
++numIterations;
double a2 = (a1 * a1) - (b1 * b1) + ax;
double b2 = (2 * a1 * b1) + ay;
if ((a2 * a2) + (b2 * b2) > Limit)
break;
++numIterations;
a1 = (a2 * a2) - (b2 * b2) + ax;
b1 = (2 * a2 * b2) + ay;
if ((a1 * a1) + (b1 * b1) > Limit)
break;
} while (numIterations < MaxIterations);
if (numIterations < MaxIterations) {
*scanLine++ = colormap[numIterations % ColormapSize];
allBlack = false;
} else {
*scanLine++ = qRgb(0, 0, 0);
}
}
}
if (allBlack && pass == 0) {
pass = 4;
} else {
if (!restart)
emit renderedImage(image, scaleFactor);
++pass;
}
}
Luego viene el núcleo del algoritmo. En lugar de tratar de crear una imagen perfecta del conjunto de Mandelbrot, hacemos varias pasadas y generamos más y más precisas (y computacionalmente costosas) aproximaciones del fractal.
Si descubrimos dentro del bucle que la variable restart se ha establecido en true (por render ()), rompemos el bucle de inmediato, por lo que el control vuelve rápidamente a la parte superior del bucle externo (el bucle forever) y buscamos a los nuevos parámetros de renderizado. Del mismo modo, si descubrimos que la variable abort se ha establecido en true (por el destructor RenderThread), salimos de la función de inmediato, terminando el hilo.
El algoritmo principal está más allá del alcance de este tutorial.
mutex.lock();
if (!restart)
condition.wait(&mutex);
restart = false;
mutex.unlock();
}
}
Una vez que hemos terminado con todas las iteraciones, llamamos QWaitCondition::wait() para poner el hilo a dormir mediante el llamado, a menos que la variable restart tenga valor true. No tiene sentido mantener un subproceso de trabajo en bucle indefinidamente mientras no hay nada que hacer.
uint RenderThread::rgbFromWaveLength(double wave)
{
double r = 0.0;
double g = 0.0;
double b = 0.0;
if (wave >= 380.0 && wave <= 440.0) {
r = -1.0 * (wave - 440.0) / (440.0 - 380.0);
b = 1.0;
} else if (wave >= 440.0 && wave <= 490.0) {
g = (wave - 440.0) / (490.0 - 440.0);
b = 1.0;
} else if (wave >= 490.0 && wave <= 510.0) {
g = 1.0;
b = -1.0 * (wave - 510.0) / (510.0 - 490.0);
} else if (wave >= 510.0 && wave <= 580.0) {
r = (wave - 510.0) / (580.0 - 510.0);
g = 1.0;
} else if (wave >= 580.0 && wave <= 645.0) {
r = 1.0;
g = -1.0 * (wave - 645.0) / (645.0 - 580.0);
} else if (wave >= 645.0 && wave <= 780.0) {
r = 1.0;
}
double s = 1.0;
if (wave > 700.0)
s = 0.3 + 0.7 * (780.0 - wave) / (780.0 - 700.0);
else if (wave < 420.0)
s = 0.3 + 0.7 * (wave - 380.0) / (420.0 - 380.0);
r = pow(r * s, 0.8);
g = pow(g * s, 0.8);
b = pow(b * s, 0.8);
return qRgb(int(r * 255), int(g * 255), int(b * 255));
}
La función rgbFromWaveLength () es una función auxiliar que convierte una longitud de onda a un valor RGB compatible con QImages 32 bits. Se llama desde el constructor para inicializar el arreglo de mapa de colores con colores agradables.
Definición de la clase MandelbrotWidget
La clase MandelbrotWidget utiliza RenderThread para dibujar el conjunto de Mandelbrot en la pantalla. Aquí está la definición de la clase:
class MandelbrotWidget : public QWidget
{
Q_OBJECT
public:
MandelbrotWidget(QWidget *parent = 0);
protected:
void paintEvent(QPaintEvent *event);
void resizeEvent(QResizeEvent *event);
void keyPressEvent(QKeyEvent *event);
#ifndef QT_NO_WHEELEVENT
void wheelEvent(QWheelEvent *event);
#endif
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
private slots:
void updatePixmap(const QImage &image, double scaleFactor);
void zoom(double zoomFactor);
private:
void scroll(int deltaX, int deltaY);
RenderThread thread;
QPixmap pixmap;
QPoint pixmapOffset;
QPoint lastDragPos;
double centerX;
double centerY;
double pixmapScale;
double curScale;
};
El widget reimplementa muchos controladores de eventos de QWidget. Además, cuenta con un método slot updatePixmap () que realiza la conexión a la señal renderedImage () del subproceso de trabajo para actualizar la pantalla cada vez que lleguen nuevos datos desde el hilo.
Entre las variables privadas, tenemos hilo de tipo RenderThread y mapa de píxeles, que contiene la última imagen renderizada.
Implementación de la clase MandelbrotWidget
const double DefaultCenterX = -0.637011f;
const double DefaultCenterY = -0.0395159f;
const double DefaultScale = 0.00403897f;
const double ZoomInFactor = 0.8f;
const double ZoomOutFactor = 1 / ZoomInFactor;
const int ScrollStep = 20;
La aplicación se inicia con algunas constantes que vamos a necesitar más adelante.
MandelbrotWidget::MandelbrotWidget(QWidget *parent)
: QWidget(parent)
{
centerX = DefaultCenterX;
centerY = DefaultCenterY;
pixmapScale = DefaultScale;
curScale = DefaultScale;
connect(&thread, SIGNAL(renderedImage(QImage,double)), this, SLOT(updatePixmap(QImage,double)));
setWindowTitle(tr("Mandelbrot"));
#ifndef QT_NO_CURSOR
setCursor(Qt::CrossCursor);
#endif
resize(550, 400);
}
La parte interesante del constructor es la llamada a los métodos qRegisterMetaType() y QObject::connect(). Vamos a empezar con la llamada al connect().
Aunque se parece a una conexión de señal-ranura estándar entre dos QObjects, ya que la señal se emite en un subproceso distinto al subproceso donde vive el receptor, la conexión es efectivamente una conexión en cola (queued connection). Estas conexiones son asíncronas (es decir, no-bloqueantes), significa que el slot será llamado en algún momento después de la declaración emit. Lo que es más, la ranura se invocará en el hilo en el que el receptor vive. Aquí, la señal se emite en el subproceso de trabajo, y la ranura se ejecuta en el hilo GUI cuando el control vuelve al bucle de eventos.
Con conexiones en cola, Qt debe guardar una copia de los argumentos que se han pasado a la señal para que pueda pasarlos a la ranura más adelante. Qt sabe tomar de copia de muchos tipos de C + + y Qt, pero QImage no es uno de ellos. Por tanto, debemos llamar a la función de plantilla qRegisterMetaType() antes de que podamos utilizar QImage como parámetro de conexiones de cola.
void MandelbrotWidget::paintEvent(QPaintEvent * /* event */)
{
QPainter painter(this);
painter.fillRect(rect(), Qt::black);
if (pixmap.isNull()) {
painter.setPen(Qt::white);
painter.drawText(rect(), Qt::AlignCenter, tr("Rendering initial image, please wait..."));
return;
}
En paintEvent(), se comienza por llenar el fondo con negro. Si no tenemos nada aún para pintar (mapa de pixels es nulo), se muestra un mensaje en el widget que pide al usuario que ser paciente y termina la función de inmediato.
if (curScale == pixmapScale) {
painter.drawPixmap(pixmapOffset, pixmap);
} else {
double scaleFactor = pixmapScale / curScale;
int newWidth = int(pixmap.width() * scaleFactor);
int newHeight = int(pixmap.height() * scaleFactor);
int newX = pixmapOffset.x() + (pixmap.width() - newWidth) / 2;
int newY = pixmapOffset.y() + (pixmap.height() - newHeight) / 2;
painter.save();
painter.translate(newX, newY);
painter.scale(scaleFactor, scaleFactor);
QRectF exposed = painter.matrix().inverted().mapRect(rect()).adjusted(-1, -1, 1, 1);
painter.drawPixmap(exposed, pixmap, exposed);
painter.restore();
}
Si el mapa de píxeles tiene el factor de escala correcto, podemos extraer el mapa de píxeles directamente sobre el widget. De lo contrario, podemos aumentar la escala y traducimos el sistema de coordenadas (coordinate system) antes de sacar el mapa de pixels. Mediante el mapeo inverso rectángulo del widget usando la matriz de pintado a escala, también nos aseguramos de que sólo las áreas expuestas del mapa de píxeles se dibujan. Las llamadas a QPainter::save() y QPainter::restore() aseguran de que todo el proceso de pintado que se realiza después utiliza el sistema de coordenadas estándar.
QString text = tr("Use mouse wheel or the '+' and '-' keys to zoom. "
"Press and hold left mouse button to scroll.");
QFontMetrics metrics = painter.fontMetrics();
int textWidth = metrics.width(text);
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(0, 0, 0, 127));
painter.drawRect((width() - textWidth) / 2 - 5, 0, textWidth + 10, metrics.lineSpacing() + 5);
painter.setPen(Qt::white);
painter.drawText((width() - textWidth) / 2, metrics.leading() + metrics.ascent(), text);
}
Al final del controlador de eventos de pintura, trazamos una cadena de texto y un rectángulo semitransparente en la parte superior del fractal.
void MandelbrotWidget::resizeEvent(QResizeEvent * /* event */)
{
thread.render(centerX, centerY, curScale, size());
}
Cada vez que el usuario cambia el tamaño del widget, llamamos al método render () para comenzar a generar una nueva imagen, con los mismo parámetros: centerX, CenterY y curScale pero con el nuevo tamaño de widget.
Nótese que confiamos en que resizeEvent () se llama automáticamente por Qt cuando el widget se muestra la primera vez para generar la imagen del primer momento.
void MandelbrotWidget::keyPressEvent(QKeyEvent *event)
{
switch (event->key()) {
case Qt::Key_Plus:
zoom(ZoomInFactor);
break;
case Qt::Key_Minus:
zoom(ZoomOutFactor);
break;
case Qt::Key_Left:
scroll(-ScrollStep, 0);
break;
case Qt::Key_Right:
scroll(+ScrollStep, 0);
break;
case Qt::Key_Down:
scroll(0, -ScrollStep);
break;
case Qt::Key_Up:
scroll(0, +ScrollStep);
break;
default:
QWidget::keyPressEvent(event);
}
}
El manejador del evento de presionar una tecla proporciona algunas asociaciones de teclas para el beneficio de los usuarios que no disponen de un ratón. Las funciones zoom () y scroll () serán cubiertos más adelante.
void MandelbrotWidget::wheelEvent(QWheelEvent *event)
{
int numDegrees = event->delta() / 8;
double numSteps = numDegrees / 15.0f;
zoom(pow(ZoomInFactor, numSteps));
}
El controlador de eventos de la rueda es reimplementada para hacer el control nivel de zoom con la rueda del ratón. QWheelEvent::delta() devuelve el ángulo del movimiento de la rueda del ratón, en octavos de un grado. Para la mayoría de los ratones, un paso de la rueda corresponde a 15 grados. Averiguamos cuántos pasos ratón tenemos y determinamos el factor de zoom en consecuencia. Por ejemplo, si tenemos dos pasos de rueda en la dirección positiva (es decir, 30 grados), el factor de zoom se vuelve ZoomInFactor a la segunda potencia, es decir, 0,8 * 0,8 = 0,64.
void MandelbrotWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
lastDragPos = event->pos();
}
Cuando el usuario pulsa el botón izquierdo del ratón, almacenamos la posición del puntero del ratón en lastDragPos.
void MandelbrotWidget::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton) {
pixmapOffset += event->pos() - lastDragPos;
lastDragPos = event->pos();
update();
}
}
Cuando el usuario mueve el puntero del ratón mientras se mantiene pulsado el botón izquierdo del ratón, ajustamos pixmapOffset para pintar el mapa de pixels en una posición desplazada y se llama al QWidget::update() para forzar un repintado.
void MandelbrotWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
pixmapOffset += event->pos() - lastDragPos;
lastDragPos = QPoint();
int deltaX = (width() - pixmap.width()) / 2 - pixmapOffset.x();
int deltaY = (height() - pixmap.height()) / 2 - pixmapOffset.y();
scroll(deltaX, deltaY);
}
}
Al soltar el botón izquierdo del ratón, actualizamos pixmapOffset tal como lo hicimos en un movimiento del ratón y reiniciamos lastDragPos a un valor predeterminado. A continuación, hacemos un llamado de scroll () para representar una nueva imagen para la nueva posición. (El ajuste de pixmapOffset no es suficiente, ya que las áreas reveladas al arrastrar el mapa de píxeles se dibujan en negro.)
void MandelbrotWidget::updatePixmap(const QImage &image, double scaleFactor)
{
if (!lastDragPos.isNull())
return;
pixmap = QPixmap::fromImage(image);
pixmapOffset = QPoint();
lastDragPos = QPoint();
pixmapScale = scaleFactor;
update();
}
El slot updatePixmap () se invoca cuando el subproceso de trabajo ha terminado de representar una imagen. Empezamos comprobando si un lastre está en vigor y no hacemos nada en ese caso. En el caso normal, guardamos la imagen en mapa de pixels e inicializar algunos de los otros miembros. Al final, llamamos QWidget::update() para actualizar la pantalla.
En este punto, uno podría preguntarse por qué usamos un QImage para el parámetro y un QPixmap para el miembro de datos. ¿Por qué no se adhieren a un tipo? La razón es que QImage es la única clase que admite la manipulación de píxeles directa, lo que necesitamos en el subproceso de trabajo. Por otro lado, antes de que una imagen se pueda dibujar en la pantalla, debe ser convertida en un mapa de pixels. Es mejor hacer la conversión de una vez por todas aquí, y no en paintEvent ().
void MandelbrotWidget::zoom(double zoomFactor)
{
curScale *= zoomFactor;
update();
thread.render(centerX, centerY, curScale, size());
}
En el zoom (), se recalcula curScale. Entonces llamamos QWidget::update() para dibujar un mapa de pixels a escala, y le pedimos al subproceso de trabajo para hacer una nueva imagen correspondiente al nuevo valor curScale.
void MandelbrotWidget::scroll(int deltaX, int deltaY)
{
centerX += deltaX * curScale;
centerY += deltaY * curScale;
update();
thread.render(centerX, centerY, curScale, size());
}
scroll() es similar a zoom(), excepto que los parámetros afectados son centerX y CenterY.
La función main()
La naturaleza multiproceso de la aplicación no tiene impacto en su función main (), que es tan simple como siempre:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MandelbrotWidget widget;
widget.show();
return app.exec();
}
El código fuente de esta aplicación pueden descargarlo desde aquí.
Esto es todo por hoy, esperamos que este artículo le haya sido útil para aprender a usar un hilo trabajador para realizar operaciones computacionales complejas sin bloquear el hilo principal del ciclo de eventos. En próximas entradas de nuestro blog estaremos profundizando sobre otros temas de programación concurrente usando el framework Qt, que permite grandes potencialidades para el desarrollo de aplicaciones con excelente rendimiento.