Mostrando entradas con la etiqueta concurrente. Mostrar todas las entradas
Mostrando entradas con la etiqueta concurrente. Mostrar todas las entradas

lunes, 14 de abril de 2014

Ejemplo Mandelbrot, programación concurrente con el Framework Qt.

clip_image002

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].

clip_image004

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.

clip_image006clip_image008 clip_image009

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.

clip_image011 clip_image013 clip_image015

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.




{ Leer Más }


viernes, 28 de marzo de 2014

Principios básicos de la programación concurrente usando el Framework Qt.

clip_image002

El presente artículo tiene el objetivo de mostrar los principios básicos de la programación multi-hilo usando el Framework Qt.

Los hilos permiten hacer que los sistemas ejecuten tareas en paralelo, al igual que procesos. Entonces, ¿cómo se diferencian los hilos de los procesos? Mientras que usted está haciendo los cálculos en una hoja de cálculo, puede haber también un reproductor multimedia que se ejecuta en el mismo escritorio tocando su canción favorita. Aquí hay un ejemplo de dos procesos trabajando en paralelo: uno que ejecuta el programa de hoja de cálculo, una ejecución de un reproductor de música. La multitarea es un término muy conocido para esto. Una mirada más cercana al reproductor de música revela que allí son cosas que suceden en paralelo dentro de un único proceso. Mientras reproductor de música realiza el envío de la música para el controlador de audio, la interfaz de usuario con todas sus campanas y silbatos se actualiza constantemente. Para esto son los hilos - la concurrencia dentro de un único proceso [1].

Entonces, ¿cómo se implementa la concurrencia? Trabajo paralelo en las CPU de un solo núcleo, es una ilusión que es algo similar a la ilusión de imágenes en movimiento en el cine. Para los procesos, la ilusión se produce mediante la interrupción de trabajo del procesador en un proceso después de un tiempo muy corto. A continuación, el procesador se mueve al siguiente proceso. Para cambiar entre procesos, el contador de programa actual se guarda y contador de programa del próximo procesador se carga. Esto no es suficiente, ya que el mismo hay que hacer con los registros y cierta arquitectura y datos específicos del sistema operativo.

Así como una CPU puede alimentar dos o más procesos, también es posible dejar que la CPU ejecute en dos segmentos de código diferentes de un único proceso. Cuando se inicia un proceso, siempre se ejecuta un segmento de código y por lo tanto, se dice que el proceso ejecuta un hilo. Sin embargo, el programa puede optar por iniciar un segundo hilo. Entonces, dos secuencias de códigos diferentes se procesan simultáneamente dentro de un solo proceso. La concurrencia se logra en las CPU de un solo núcleo guardando repetidamente contadores de programa y luego los registros cargan contadores y registros del programa del próximo hilo. No se requiere la cooperación del programa para desplazarse entre los hilos activos. Un hilo puede estar en cualquier estado cuando se produce el cambio al siguiente contexto.

La tendencia actual en el diseño de la CPU es tener varios núcleos. Una aplicación de un único subproceso típico puede hacer uso de un solo núcleo. Sin embargo, un programa con múltiples hilos se puede asignar a varios núcleos, lo que hace que las cosas sucedan de una manera verdaderamente concurrente. Como resultado, la distribución del trabajo a más de un hilo puede hacer que un programa funcione mucho más rápido en las CPU multinúcleo debido a que núcleos adicionales pueden ser utilizados.

Hilo GUI e Hilo Trabajador

Como se mencionó, cada programa tiene un hilo cuando se inicia. Este hilo se llama el "hilo conductor" (también conocido como el "hilo GUI" en aplicaciones Qt). La interfaz gráfica de usuario Qt se debe ejecutar en este hilo. Todos los widgets y varias clases relacionadas, por ejemplo QPixmap, no trabajan en los hilos secundarios. Un subproceso secundario que comúnmente se conoce como un "subproceso de trabajo" ya que se utiliza para descargar el trabajo de procesamiento del hilo principal.

El acceso simultáneo a los datos

Cada hilo tiene su propia pila, lo que significa que cada hilo tiene su propio historial de llamadas y variables locales. A diferencia de los procesos, los hilos comparten el mismo espacio de direcciones. El siguiente diagrama muestra cómo se encuentran los bloques de construcción de los hilos en la memoria. Contador y los registros de hilos inactivos Programa suelen mantenerse en el espacio del núcleo. Hay una copia compartida del código y una pila separada para cada hilo.

clip_image004

Si dos hilos tienen un puntero al mismo objeto, es posible que los dos hilos tengan acceso a dicho objeto al mismo tiempo y esto potencialmente puede destruir la integridad del objeto. Es fácil imaginar las muchas cosas que pueden salir mal cuando dos métodos del mismo objeto se ejecutan simultáneamente.

A veces es necesario para acceder a un objeto a partir de diferentes hilos, por ejemplo, cuando los objetos que viven en diferentes hilos necesitan comunicarse. Desde hilos utilizan el mismo espacio de direcciones, es más fácil y más rápido para hilos para intercambiar datos de lo que es para los procesos. Los datos no tiene que ser serializado y copiado. Pasar punteros es posible, pero debe haber una coordinación estricta de lo que toques de hilo que se oponen. La ejecución simultánea de las operaciones en un objeto debe ser prevenida. Hay varias maneras de lograr esto y algunas de ellos se describen a continuación.

Entonces, ¿Cómo se pueden programar estas aplicaciones de manera segura? Todos los objetos creados en un hilo se pueden utilizar de manera segura dentro de ese hilo, siempre que otros hilos no tengan referencias a los mismos y los objetos no tienen acoplamiento implícito con otros hilos. Tal acoplamiento implícito puede suceder cuando los datos son compartidos entre instancias como miembros estáticos, singletons o datos globales. Debe familiarizarse con el concepto de clase hilo y funciones seguras y de reentrada.

Uso de hilos

Básicamente, existen dos escenarios fundamentales para el uso de los hilos:

  • Hacer un procesamiento más rápido, haciendo uso de los procesadores multinúcleo.
  • Mantener el hilo GUI u otros hilos críticos liberados al descargar el procesamiento de larga duración o el bloqueo de llamadas a otros hilos.


¿Qué Tecnología Qt se debe usar para el manejo de hilos?

A veces quieres hacer algo más que la ejecución de un método en el contexto de otro hilo. Es posible que desee tener un objeto que vive en otro hilo que proporciona un servicio al hilo GUI. Tal vez usted quiere otro hilo que siga con vida para siempre para sondear los puertos de hardware y enviar una señal al hilo GUI cuando algo notable ha sucedido. Qt proporciona diferentes soluciones para el desarrollo de aplicaciones con subprocesos. La solución correcta depende de la finalidad del nuevo hilo, así como el tiempo de vida que se requiere de ese hilo:

Vida útil del hilo

Tarea de Desarrollo

Solución

Una llamada

Ejecutar un método dentro de otro hilo y dejar el hilo cuando termine el método.

Qt proporciona diferentes soluciones:

Escribir una función y ejecutarla con QtConcurrent::run()

Derivar una clase de QRunnable y ejecutarla en el hilo global con QThreadPool::globalInstance()->start()

Derivar una clase de QThread, reimplementar el método QThread::run() y utilizar QThread::start() para ejecutarlo.

Una llamada

Operaciones se han de realizar en todos los hilos de un contenedor. El procesamiento deberá ser realizado utilizando todos los núcleos disponibles. Un ejemplo común es la producción de imágenes en miniatura de una lista de imágenes.

QtConcurrent proporciona la función de mapa () para aplicar las operaciones en cada elemento contenedor, filtro () para la selección de elementos de recipiente, y la opción de especificar una función de reducir para combinar los elementos restantes.

Una llamada

Una operación de larga duración tiene que ser puesto en otro hilo. Durante el curso de procesamiento, información de estado se debe enviar al hilo de la interfaz gráfica de usuario.

Utilice QThread, reimplementar run() y también las signal/slot sean necesarias. Conecte las señales a las ranuras (slots) del hilo GUI utilizando conexiones de señal / ranura (signal/slot) en cola.

Permanente

Tener un objeto de estar en otro hilo y dejar que se realicen diferentes tareas bajo petición. Esto significa que la comunicación hacia y desde el subproceso de trabajo que se requiere.

Derivar una clase de QObject e implementar las ranuras y las señales necesarias, mover el objeto a un hilo con un bucle de eventos de correr y comunicarse con el objeto a través de conexiones de señal / ranura en cola.

Permanente

Tener un objeto que vive en otro hilo, dejar al objeto realizar tareas repetitivas, como el sondeo de puertos y permitir la comunicación con el hilo GUI.

Igual que el anterior, pero también utilizar un temporizador en el subproceso de trabajo para implementar la encuesta. Sin embargo, la mejor solución para la encuesta es evitarla por completo. A veces, usar QSocketNotifier es una alternativa.

En próximas entradas estaremos profundizando sobre otros temas de programación concurrente usando el framework Qt.

{ Leer Más }


viernes, 30 de agosto de 2013

Programación concurrente usando Semáforos con el Framework Qt.

clip_image002

El presente artículo tiene el objetivo de mostrar las bondades de la programación multi-hilo usando el Framework Qt.

Se utilizara la tecnología de Semáforos a través de la clase QSemaphore para controlar el acceso a un búfer circular que comparten un hilo productor y un hilo consumidor.

El productor escribe datos en el búfer hasta que se llega al final del buffer, en cuyo punto se reinicia desde el principio, sobrescribiendo los datos existentes. El hilo consumidor lee los datos a medida que se produce y lo escribe en la salida de error estándar.

Los semáforos hacen posible tener un mayor nivel de concurrencia que los mutex. Si los accesos a la memoria intermedia fueran vigilados por un QMutex, el hilo consumidor no podría acceder a la memoria intermedia al mismo tiempo que el hilo productor. Sin embargo, no hay nada malo en tener dos hilos que trabajan en diferentes partes de la memoria intermedia en el mismo tiempo.

El ejemplo consta de dos clases: productor y consumidor. Ambos heredan de QThread. La memoria intermedia circular utilizado para la comunicación entre estas dos clases y los semáforos que lo protegen son variables globales.

Una alternativa al uso QSemaphore para resolver el problema del productor-consumidor es utilizar QWaitCondition y QMutex.

Variables globales

Vamos a empezar por la revisión del buffer circular y los semáforos asociados:

const int DataSize = 100000;
const int BufferSize = 8192;

char buffer[BufferSize];
QSemaphore freeBytes(BufferSize);
QSemaphore usedBytes;

DataSize es la cantidad que de los datos que el productor va a generar. Para mantener el ejemplo tan simple como sea posible, será una constante. BufferSize es el tamaño del buffer circular. Es menos de DataSize, lo que significa que en algún momento el productor alcanzará el final del buffer y reiniciar desde el principio.

Para sincronizar el productor y el consumidor, necesitamos dos semáforos. El semáforo freeBytes controla el área "libre" del búfer (el área que el productor no ha llenado con los datos todavía o que el consumidor ya ha leído). El semáforo usedBytes controla el área "usado" de la memoria intermedia (el área que el productor ha llenado pero que el consumidor todavía no ha leído).

Juntos, los semáforos garantizan que el productor nunca este más de bufferSize por delante del consumidor, y que el consumidor nunca lee los datos que el productor no ha generado todavía.

El semáforo freeBytes se inicializa con BufferSize, porque al principio todo el buffer está vacío. El semáforo usedBytes se inicializa a 0 (el valor por defecto si no se especifica ninguno).

Clase Productor

Repasemos el código de la clase Producer:

class Producer : public QThread
{
public:
void run()
{
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
for (int i = 0; i < DataSize; ++i) {
freeBytes.acquire();
buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];
usedBytes.release();
}
}
};

El productor genera DataSize bytes de datos. Antes de que escriba un byte en el buffer circular, se debe adquirir un byte "libre" con el semáforo freeBytes. La llamada al QSemaphore :: adquirir () puede bloquear si el consumidor no ha mantenido el ritmo con el productor.

Al final, el productor libera un byte utilizando el semáforo usedBytes. El byte "libre" con éxito se ha transformado en un byte "usado", listo para ser leído por el consumidor.

Clase del Consumidor

Pasemos ahora a la clase de los consumidores:

class Consumer : public QThread
{
Q_OBJECT
public:
void run()
{
for (int i = 0; i < DataSize; ++i) {
usedBytes.acquire();
fprintf(stderr, "%c", buffer[i % BufferSize]);
freeBytes.release();
}
fprintf(stderr, "\n");
}

signals:
void stringConsumed(const QString &text);

protected:
bool finish;
};

El código es muy similar a la del productor, excepto que esta vez que adquirimos un byte "usado" y liberar un byte "libre", en lugar de lo contrario.

La función main ()

En main (), creamos los dos hilos y llamamos QThread :: wait () para asegurarse de que ambos contextos tienen tiempo para terminar antes de la salida:

int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}

Entonces, ¿qué sucede cuando ejecutamos el programa? Inicialmente, el hilo productor es el único que puede hacer cualquier cosa, el consumidor está bloqueado esperando el semáforo usedBytes se libere (a su disposición inicial () count es 0). Una vez que el productor ha puesto un byte en el buffer, freeBytes.available () es BufferSize - 1 y usedBytes.available () es 1. En ese momento, pueden ocurrir dos cosas: o bien el hilo consumidor se hace cargo y dice que el byte, o si el cliente llega a producir un segundo byte.

El modelo productor-consumidor se presenta en este ejemplo permite escribir aplicaciones multiproceso altamente concurrentes. En un equipo con varios procesadores, el programa es potencialmente hasta el doble de rápido que el programa de exclusión mutua basado equivalente, ya que los dos hilos pueden estar activos al mismo tiempo en diferentes partes del buffer.

Tenga en cuenta que estos beneficios no siempre son efectivos. La adquisición y liberación de un QSemaphore tiene un costo. En la práctica, probablemente valdría la pena dividir la memoria intermedia en trozos y para operar en trozos en lugar de bytes individuales. El tamaño del búfer es también un parámetro que se debe seleccionar cuidadosamente, basado en la experimentación.

El código fuente de esta aplicación pueden descargarlo desde aquí.




{ Leer Más }


IconIconIcon