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í.
salidos, como usar un semaforo concurrente con una conexion ip o un socket haciendo pase de mensajes entre un servidor y un cliente
ResponderEliminar