En GNU/Linux se pueden crear threads con la librería p_thread, que como ya sabéis forma parte del estándar POSIX. Cuando hablamos de Windows la cosa cambia, ya que Windows tiene su forma propia de crear Threads utilizando la WIN API. En esta entrada profundizaremos y aprenderemos a crear threads en C con Windows.
La WIN API es el API de Windows de más bajo nivel que nos permite utilizar todas aquellas características propias del Sistema Operativo.
La entrada constará de 7 ejemplos prácticos con sus debidas explicaciones donde empezaremos creando Threads básicos y acabaremos realizando un ejemplo de Productores-Consumidores, y por lo tanto, revisando el concepto de Sección Crítica. Vamos al lío!
¿ Qué es un Thread ?
Básicamente un Thread es un flujo de ejecución que el procesador ejecuta y que siempre forma parte de un proceso. Los procesos suelen estar constituidos de muchos Threads y esto nos permite paralelizar acciones, lo cuál nos permite aprovechar el hardware actual del que disponemos en nuestros equipos y que suele estar compuesto por varios núcleos. Para que os hagáis una idea, si tenemos un procesador con 4 núcleos podremos ejecutar 4 tareas en paralelo en un determinado momento. Esto no quiere decir que nuestro procesador empiece a ejecutar un Thread y hasta que no lo acabe no pase a hacer otra cosa, que va, la ejecución de los threads los va interrumpiendo el Sistema Operativo por numerosas razones como las interrupciones, Threads que llegan con mayor prioridad, etc… Pero como resumen, podemos decir que un procesador puede estar ejecutando en un momento determinado instrucciones que pertenecen a n threads diferentes siendo n igual al número de núcleos de nuestro procesador.
¿ Para qué sirven los Threads ?
Los Threads nos permiten paralelizar tareas y por lo tanto conseguir principalmente 2 cosas:
- Acelerar el tiempo de ejecución de nuestros programas.
- Conseguir que nuestro programa realice diferentes cosas en paralelo.
¿ Cómo se crean Threads en Windows ?
Veamos el código y luego expliquémoslo:
#include <stdio.h>
#include <windows.h>
DWORD WINAPI ThreadFunc(void* data) {
printf("Hola desde un hilo de Windows!\n");
return 0;
}
int main() {
HANDLE thread = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL);
//Si se ha podido crear el Thread, esperamos a que termine
if (thread) {
//Esperamos a que termine el thread
WaitForSingleObject(thread, INFINITE);
}
printf("Ha terminado el thread y ahora va a terminarse el programa principal!\n");
}
La función «ThreadFunc» será nuestro Thread, este puede recibir parámetros para su ejecución como veremos en otros ejemplos posteriormente. Si nos fijamos en el método main, realizamos las siguientes acciones:
- Llamamos a la función «CreateThread», que nos devuelve un manejador «HANDLER» que nos permitirá trabajar con el Thread.
- Si «thread» es diferente de NULL quiere decir que hemos podido crear nuestro Thread y que por lo tanto, este ya se está ejecutando.
- Mediante la llamada a la función «WaitForSingleObject» pasándole como parámetros el Handler del Thread y un tiempo de espera podemos hacer que el hilo principal espere a que acabe nuestro hilo secundario. Recordad que nuestro hilo principal ejecuta la función main y nuestro hilo secundario la función «ThreadFunc».
La salida de nuestro programa
Hola desde un hilo de Windows!
Ha terminado el thread y ahora va a terminarse el programa principal!
La función CreateThread la podéis ver definida aquí, la información que nos importa es la siguiente:
HANDLE CreateThread(
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in, optional] __drv_aliasesMem LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out, optional] LPDWORD lpThreadId
);
- El primer parámetro son los atributos de Seguridad que le queremos asignar al Thread. En esta entrada no los utilizamos, son opcionales y quedan fuera de lo que queremos enseñar en esta entrada.
- El segundo parámetro nos permite definir el tamaño inicial de la pila que utilizará nuestro Thread, si asignamos un valor de 0 se coge el valor por defecto de Windows.
- El tercer parámetro es la dirección de memoria de la función que define nuestro Thread.
- El cuarto parámetro son los parámetros de entrada que queremos pasarle al Thread.
- El quinto parámetro nos permite definir como se crea el Thread, en nuestro caso pasamos un 0 porque queremos que nuestro Thread se ejecute inmediatamente. Con otros flags se puede crear hilos suspendidos, etc…
- El sexto parámetro no permite pasar un puntero donde se asignará el id que Windows ha asignado a nuestro Thread. Luego veremos esto mediante un ejemplo.
Para hacer que el hilo que ejecuta nuestro main espere a que termine nuestro Thread hemos ejecutado la función «WaitForSingleObject», que la podéis ver definida aquí
Lo más importante es lo siguiente:
DWORD WaitForSingleObject(
[in] HANDLE hHandle,
[in] DWORD dwMilliseconds
);
Simplemente le tenemos que pasar el Handler de nuestro Thread, handler que nos ha devuelto la función «CreateThread» y el tiempo que queremos esperar, en nuestro caso le pasamos «INFINITE», de esa forma esperamos sí o sí a que acabe nuestro Thread Secundario.
Si os habéis fijado la salida del ejemplo anterior primero se ejecutaba el printf que se hallaba dentro del Thread Secundario y después el printf que hay después de que terminase de ejecutarse nuestro Thread:
Hola desde un hilo de Windows!
Ha terminado el thread y ahora va a terminarse el programa principal!
Como realmente nuestro hilo solo ejecuta un printf (se ejecuta muy rápido), realmente podría dar la casualidad de que no hubiéramos hecho bien la parte de hacer que el hilo que ejecuta nuestro método main espere a nuestro hilo secundario, para comprobar que lo hemos realizado correctamente vamos a poner un «Sleep»
#include <stdio.h>
#include <windows.h>
DWORD WINAPI ThreadFunc(void* data) {
printf("Hola desde un hilo de Windows! Vamos a esperar 5 segundos\n");
//Ponemos un sleep para comprobar que realmente esperamos a que termine nuestro thread para que se termine el programa
Sleep(5000);
return 0;
}
int main() {
HANDLE thread = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL);
//Si se ha podido crear el Thread, esperamos a que termine
if (thread) {
//Esperamos a que termine el thread
WaitForSingleObject(thread, INFINITE);
}
printf("Ha terminado el thread y ahora va a terminarse el programa principal!\n");
}
La salida de nuestro programa
Hola desde un hilo de Windows!
Ha terminado el thread y ahora va a terminarse el programa principal!
¿ Cómo se le pasan parámetros a un Hilo en Windows ?
Veamos el código:
#include <stdio.h>
#include <windows.h>
int resultado = -1;
DWORD WINAPI ThreadFunc(void* data) {
//Casteamos nuestros parametros de entrada a puntero de tipo *int;
int numero1 = *((int*)data);
int numero2 = *((int*)data + 1);
resultado = numero1 + numero2;
return 0;
}
int main() {
//Creamos los parámetros que le vamos a pasar al thread
int *parametros = calloc(2, sizeof(int));
*(parametros) = 5;
*(parametros + 1) = 10;
HANDLE thread = CreateThread(NULL, 0, ThreadFunc, (void*)parametros, 0, NULL);
//Si se ha podido crear el Thread, esperamos a que termine
if (thread) {
//Esperamos a que termine el thread
WaitForSingleObject(thread, INFINITE);
//Obtenemos el valor que ha calculado el thread
printf("El resultado de la suma es: %d\n", resultado);
}
//Liberamos la memoria
free(parametros);
printf("Ha terminado el thread y ahora va a terminarse el programa principal!\n");
}
Si os fijáis la función que representa nuestro Thread «ThreadFunc» tiene un parámetro de entrada, un puntero de tipo «void», ThreadFunc(void* data). Es mediante este puntero con el cual se pasan parámetros a nuestro Thread:
//Creamos los parámetros que le vamos a pasar al thread
int *parametros = calloc(2, sizeof(int));
*(parametros) = 5;
*(parametros + 1) = 10;
HANDLE thread = CreateThread(NULL, 0, ThreadFunc, (void*)parametros, 0, NULL);
Hemos creado un Thread que sumará 2 números, por lo tanto, vamos a crear un puntero de tipo int* y vamos a reservar memoria para introducir 2 números, los que sumaremos, luego posteriormente en la función «CreateThread» pasaremos dicho puntero casteado a void*.
¿ Cómo obtenemos los parámetros en el Thread ?
//Casteamos nuestros parametros de entrada a puntero de tipo *int;
int numero1 = *((int*)data);
int numero2 = *((int*)data + 1);
Ahora ya podemos realizar la suma de nuestro números:
resultado = numero1 + numero2;
¿ Cómo devolvemos el resultado de nuestra suma ?
Se puede hacer de 2 formas diferentes, la primera es utilizar una variable global, que es lo que hemos realizado en el ejemplo anterior:
#include <stdio.h>
#include <windows.h>
int resultado = -1;
DWORD WINAPI ThreadFunc(void* data) {
//Casteamos nuestros parametros de entrada a puntero de tipo *int;
int numero1 = *((int*)data);
int numero2 = *((int*)data + 1);
resultado = numero1 + numero2;
return 0;
}
int main() {
//Creamos los parámetros que le vamos a pasar al thread
int *parametros = calloc(2, sizeof(int));
*(parametros) = 5;
*(parametros + 1) = 10;
HANDLE thread = CreateThread(NULL, 0, ThreadFunc, (void*)parametros, 0, NULL);
//Si se ha podido crear el Thread, esperamos a que termine
if (thread) {
//Esperamos a que termine el thread
WaitForSingleObject(thread, INFINITE);
//Obtenemos el valor que ha calculado el thread
printf("El resultado de la suma es: %d\n", resultado);
}
//Liberamos la memoria
free(parametros);
printf("Ha terminado el thread y ahora va a terminarse el programa principal!\n");
}
Nuestro Thread, por defecto tiene acceso a todas nuestras variables globales. Esta forma no es la forma más elegante de hacer las cosas, y por lo tanto, podemos utilizar otra alternativa, utilizar el puntero de entrada de parámetros al Thread para devolver el resultado:
#include <stdio.h>
#include <windows.h>
DWORD WINAPI ThreadFunc(void* data) {
//Casteamos nuestros parametros de entrada a puntero de tipo *int;
int numero1 = *((int*)data);
int numero2 = *((int*)data + 1);
*((int*)data + 2) = numero1 + numero2;
return 0;
}
int main() {
//Creamos los parámetros que le vamos a pasar al thread
int *parametros = calloc(3, sizeof(int));
*(parametros) = 5;
*(parametros + 1) = 10;
HANDLE thread = CreateThread(NULL, 0, ThreadFunc, (void*)parametros, 0, NULL);
//Si se ha podido crear el Thread, esperamos a que termine
if (thread) {
//Esperamos a que termine el thread
WaitForSingleObject(thread, INFINITE);
//Obtenemos el valor que ha calculado el thread
printf("El resultado de la suma es: %d\n", *(parametros + 2));
}
//Liberamos la memoria
free(parametros);
printf("Ha terminado el thread y ahora va a terminarse el programa principal!\n");
}
Aquí creamos un array para almacenar los 2 números que vamos a sumar y otro más para almacenar el resultado.
//Creamos los parámetros que le vamos a pasar al thread
int *parametros = calloc(3, sizeof(int));
Realizamos la suma de la siguiente manera:
*((int*)data + 2) = numero1 + numero2;
Obtenemos el resultado de la siguiente manera:
//Esperamos a que termine el thread
WaitForSingleObject(thread, INFINITE);
//Obtenemos el valor que ha calculado el thread
printf("El resultado de la suma es: %d\n", *(parametros + 2));
La salida de nuestro programa
El resultado de la suma es: 15
Ha terminado el thread y ahora va a terminarse el programa principal!
¿ Cómo obtenemos el id que nos asigna Windows a nuestro Thread ?
#include <stdio.h>
#include <windows.h>
DWORD WINAPI ThreadFunc(void* data) {
printf("Hola soy un thread! y voy a devolver mi id: %lu\n", GetCurrentThreadId());
return 0;
}
int main() {
DWORD *idThread = calloc(1, sizeof(DWORD));
//Le pasamos un puntero al thread para obtener su id
HANDLE thread = CreateThread(NULL, 0, ThreadFunc, NULL, 0, idThread);
printf("El id del thread es: %lu\n", *idThread);
//Si se ha podido crear el Thread, esperamos a que termine
if (thread) {
//Esperamos a que termine el thread
WaitForSingleObject(thread, INFINITE);
}
//Liberamos la memoria
free(idThread);
printf("Ha terminado el thread y ahora va a terminarse el programa principal!\n");
}
Si os fijáis creamos un puntero de tipo «DWORD», reservamos memoria para él, y se lo pasamos por parámetro a la función «CreateThread»
La salida de nuestro programa
El id del thread es: 24536
Hola soy un thread! y voy a devolver mi id: 24536
Ha terminado el thread y ahora va a terminarse el programa principal!
¿ Y qué hay de las secciones críticas ?
Primero definamos que es una sección crítica
¿ Qué es una sección crítica ?
Muchas veces nos interesa que nuestros Threads accedan a recursos compartidos y lo hagan de forma ordenada, por ejemplo, que sean capaces de introducir datos en un Array para que otros Threads los puedan leer. Esto se debe hacer de forma ordenada porque no podemos tener 2 hilos al mismo tiempo modificando una estructura de datos o de lo contrario el resultado puede ser totalmente inesperado, y suele serlo. La forma de controlar el acceso a los recursos compartidos es lo que llamamos sección crítica. En el siguiente ejemplo creamos 300 hilos que deben sumar 1 a un contador de forma ordenada, con el objetivo de que nuestro contador acabe en 300 cuando acabe nuestro programa, la única forma de conseguirlo es serializar las sumas, es decir, que solo pueda haber un hilo realizando la suma en un momento determinado, y para ello, utilizamos un mecanismo de exclusión mutua llamada «mutex«.
El mutex nos permite definir secciones críticas de la siguiente forma:
- Adquirimos el mutex. Si cuando nuestro hilo intenta adquirir el mutex ya hay otro hilo dentro de la sección crítica este se duerme.
- Una vez adquirido el mutex estamos dentro de la sección crítica y por lo tanto, tenemos garantizado que solo nuestro hilo va a estar dentro de esta sección por lo que podemos acceder a nuestro recurso compartido sin miedo.
- Una vez realizadas las modificaciones liberamos el mutex para que otro hilo pueda entrar a la sección crítica. Todo el código que hay entre la adquisición del mutex y su liberación es la sección crítica.
Veamos el código:
#include <stdio.h>
#include <windows.h>
// Definimos el mutex para serializar el acceso a nuestra estructura de datos
HANDLE hMutex;
int contador = 0;
DWORD WINAPI ThreadFunc(void *data)
{
// Adquirimos el mutex
DWORD dwWaitResult = WaitForSingleObject(hMutex, INFINITE);
//Sección crítica
Sleep(10);
contador++;
// Liberamos el mutex
ReleaseMutex(hMutex);
return TRUE;
}
int main()
{
// Inicializamos el mutex
hMutex = CreateMutex(
NULL, // default security attributes
FALSE, // initially not owned
NULL); // unnamed mutex
if (hMutex == NULL)
{
printf("CreateMutex error: %d\n", GetLastError());
return 1;
}
HANDLE aThreads[300];
int i;
for (i = 0; i < 300; i++)
{
aThreads[i] = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL);
if (aThreads[i] == NULL)
{
printf("CreateThread error: %d\n", GetLastError());
return 1;
}
}
for (int i = 0; i < 300; i++){
WaitForSingleObject(aThreads[i], INFINITE);
}
printf("El resultado de nuestro contador es: %d\n", contador);
// Cerramos todos los handlers, threadsd y mutex
for (i = 0; i < 300; i++)
CloseHandle(aThreads[i]);
CloseHandle(hMutex);
printf("Ha terminado el thread y ahora va a terminarse el programa principal!\n");
}
Aquí el primer paso es crear e inicializar nuestro mutex:
// Inicializamos el mutex
hMutex = CreateMutex(
NULL, // default security attributes
FALSE, // initially not owned
NULL); // unnamed mutex
if (hMutex == NULL)
{
printf("CreateMutex error: %d\n", GetLastError());
return 1;
}
Luego creamos nuestros Threads y los ponemos a funcionar:
HANDLE aThreads[300];
int i;
for (i = 0; i < 300; i++)
{
aThreads[i] = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL);
if (aThreads[i] == NULL)
{
printf("CreateThread error: %d\n", GetLastError());
return 1;
}
}
Veamos ahora que realiza nuestro Thread, ya que es el encargado de coger el mutex, realizar la suma y liberarlo:
DWORD WINAPI ThreadFunc(void *data)
{
// Adquirimos el mutex
DWORD dwWaitResult = WaitForSingleObject(hMutex, INFINITE);
//Sección crítica
Sleep(10);
contador++;
// Liberamos el mutex
ReleaseMutex(hMutex);
return TRUE;
}
Mediante la función «WaitForSingleObject» adquirimos el Mutex, y mediante la función «ReleaseMutex» lo liberamos. Entre una cosa y la otra se encuentra nuestra sección crítica, que en este ejemplo solo suma 1 a nuestro contador.
Por último, esperamos a que nuestros hilos acaben y mostramos el resultado del contador:
for (int i = 0; i < 300; i++){
WaitForSingleObject(aThreads[i], INFINITE);
}
printf("El resultado de nuestro contador es: %d\n", contador);
// Cerramos todos los handlers, threadsd y mutex
for (i = 0; i < 300; i++)
CloseHandle(aThreads[i]);
CloseHandle(hMutex);
Es importante liberar los recursos, tanto los handler de los Threads, como el handler del Mutex.
El resultado de nuestro programa
El resultado de nuestro contador es: 300
Ha terminado el thread y ahora va a terminarse el programa principal!
Vamos a probar ahora a ejecutar el mismo código comentando la sección de adquisición del mutex y su liberación, es decir, sin sección crítica:
#include <stdio.h>
#include <windows.h>
// Definimos el mutex para serializar el acceso a nuestra estructura de datos
HANDLE hMutex;
int contador = 0;
DWORD WINAPI ThreadFunc(void *data)
{
// Adquirimos el mutex
//DWORD dwWaitResult = WaitForSingleObject(hMutex, INFINITE);
//Sección crítica
Sleep(10);
contador++;
// Liberamos el mutex
//ReleaseMutex(hMutex);
return TRUE;
}
int main()
{
// Inicializamos el mutex
hMutex = CreateMutex(
NULL, // default security attributes
FALSE, // initially not owned
NULL); // unnamed mutex
if (hMutex == NULL)
{
printf("CreateMutex error: %d\n", GetLastError());
return 1;
}
HANDLE aThreads[300];
int i;
for (i = 0; i < 300; i++)
{
aThreads[i] = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL);
if (aThreads[i] == NULL)
{
printf("CreateThread error: %d\n", GetLastError());
return 1;
}
}
for (int i = 0; i < 300; i++){
WaitForSingleObject(aThreads[i], INFINITE);
}
printf("El resultado de nuestro contador es: %d\n", contador);
// Cerramos todos los handlers, threadsd y mutex
for (i = 0; i < 300; i++)
CloseHandle(aThreads[i]);
CloseHandle(hMutex);
printf("Ha terminado el thread y ahora va a terminarse el programa principal!\n");
}
Fijaos en la salida:
El resultado de nuestro contador es: 298
Ha terminado el thread y ahora va a terminarse el programa principal!
Debería de salir 300, ya que tenemos 300 threads y realizamos 300 sumas, sin embargo nos sale 298, esto es así porque hay varios hilos accediendo y realizando la suma al mismo tiempo y por lo tanto, no se realizan todas las sumas.
Siempre que vayáis a acceder a un recurso compartido utilizad secciones críticas!!!!!!!!!!!!!!!
Vamos a complicar un poca más las cosas.
Productores/Consumidores con Threads en C con Windows y la WIN API
Para esto vamos a utilizar otro mecanismo de exclusión mutua llamado «CriticalSection» y «ConditionVariables«. El supuesto funciona de la siguiente forma, tenemos 150 hilos que crearan productos y otros 150 hilos que leerán estos productos, el tiempo de producir y de leer productos será un número aleatorio entre 0 y 50 milisegundos. Tenemos que conseguir que los 150 productores puedan producir items y que los lectores puedan leerlos y no tenemos garantizado ningún orden de ejecución, por lo que tenemos que ir despertando y durmiendo los hilos. Para complicarlo un poco más, solo tenemos un Buffer de 10 elementos, y si el Buffer está lleno los productores no pueden producir más elementos hasta que haya un consumidor que vaya vaciando el Buffer. Con los consumidores nos pasa algo parecido, no podemos leer si el Buffer está vacío.
Veamos el código:
#define WINVER 0x0600
#define _WIN32_WINNT_VISTA 0x0600
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#define BUFFER_SIZE 10
LONG Buffer[BUFFER_SIZE];
ULONG QueueSize;
ULONG QueueStartOffset;
ULONG TotalItemsProducidos;
ULONG TotalItemsConsumidos;
CONDITION_VARIABLE BufferNotEmpty;
CONDITION_VARIABLE BufferNotFull;
CRITICAL_SECTION BufferLock;
DWORD WINAPI ThreadProductor(PVOID p)
{
// Entramos en la sección crítica
EnterCriticalSection(&BufferLock);
while (QueueSize == BUFFER_SIZE)
{
// El buffer esta lleno, dormimos al thread
SleepConditionVariableCS(&BufferNotFull, &BufferLock, INFINITE);
}
// Insertamos el elemento al final de la cola e incrementamos el tamaño
// En este ejemplo producimos un número, y siempre el mismo, realmente no es importante para la explicación
//Simulamos un tiempo random entre 0 y 50 que nos simule un tiempo de producción del producto
Sleep( rand() % 50);
Buffer[(QueueStartOffset + QueueSize) % BUFFER_SIZE] = 1;
QueueSize++;
TotalItemsProducidos++;
printf("Un productor ha producido un producto\n");
LeaveCriticalSection(&BufferLock);
// Si un consumidor esta esperando lo despertamos
WakeConditionVariable(&BufferNotEmpty);
return 0;
}
DWORD WINAPI ThreadConsumidor(PVOID p)
{
// Entramos en la sección crítica
EnterCriticalSection(&BufferLock);
while (QueueSize == 0)
{
// El buffer esta lleno, dormimos al thread
SleepConditionVariableCS(&BufferNotEmpty, &BufferLock, INFINITE);
}
//Nos dormimos un tiempo random entre 0 y 30 para simular lo que nos cuesta consumir un producto
Sleep( rand() % 30);
LONG Item = Buffer[QueueStartOffset];
QueueSize--;
QueueStartOffset++;
TotalItemsConsumidos++;
if (QueueStartOffset == BUFFER_SIZE)
{
QueueStartOffset = 0;
}
printf("Un consumidor ha consumido un producto\n");
LeaveCriticalSection(&BufferLock);
// Si un productor esta esperando lo despertamos
WakeConditionVariable(&BufferNotFull);
return 0;
}
int main()
{
//Generamos la semilla de random
time_t t1;
srand ( (unsigned) time (&t1));
//Inicializamos las 2 variables condicion y la sección crítica
InitializeConditionVariable(&BufferNotEmpty);
InitializeConditionVariable(&BufferNotFull);
InitializeCriticalSection(&BufferLock);
HANDLE aThreads[300];
int i;
// Creamos 150 consumidores
for (i = 0; i < 150; i++)
{
aThreads[i] = CreateThread(NULL, 0, ThreadConsumidor, NULL, 0, NULL);
if (aThreads[i] == NULL)
{
printf("CreateThread error: %d\n", GetLastError());
return 1;
}
}
// Creamos 150 productores
for (i = 150; i < 300; i++)
{
aThreads[i] = CreateThread(NULL, 0, ThreadProductor, NULL, 0, NULL);
if (aThreads[i] == NULL)
{
printf("CreateThread error: %d\n", GetLastError());
return 1;
}
}
// Esperamos a que terminen todos
for (int i = 0; i < 300; i++)
{
WaitForSingleObject(aThreads[i], INFINITE);
}
printf("Numero de items producidos: %d\n", TotalItemsProducidos);
printf("Numero de items consumidos: %d\n", TotalItemsConsumidos);
// Cerramos todos los handlers
for (i = 0; i < 300; i++)
CloseHandle(aThreads[i]);
printf("Ha terminado el thread y ahora va a terminarse el programa principal!\n");
}
Expliquemos nuestro código, lo primero que realizamos es inicializar nuestras variables condición y nuestro objeto de sección crítica:
//Inicializamos las 2 variables condicion y la sección crítica
InitializeConditionVariable(&BufferNotEmpty);
InitializeConditionVariable(&BufferNotFull);
InitializeCriticalSection(&BufferLock);
Posterior a esto creamos nuestros Threads:
HANDLE aThreads[300];
int i;
// Creamos 150 consumidores
for (i = 0; i < 150; i++)
{
aThreads[i] = CreateThread(NULL, 0, ThreadConsumidor, NULL, 0, NULL);
if (aThreads[i] == NULL)
{
printf("CreateThread error: %d\n", GetLastError());
return 1;
}
}
// Creamos 150 productores
for (i = 150; i < 300; i++)
{
aThreads[i] = CreateThread(NULL, 0, ThreadProductor, NULL, 0, NULL);
if (aThreads[i] == NULL)
{
printf("CreateThread error: %d\n", GetLastError());
return 1;
}
}
Tanto productores como consumidores. Veamos ahora los Threads
El código de nuestro Thread Productor es el siguiente:
DWORD WINAPI ThreadProductor(PVOID p)
{
// Entramos en la sección crítica
EnterCriticalSection(&BufferLock);
while (QueueSize == BUFFER_SIZE)
{
// El buffer esta lleno, dormimos al thread
SleepConditionVariableCS(&BufferNotFull, &BufferLock, INFINITE);
}
// Insertamos el elemento al final de la cola e incrementamos el tamaño
// En este ejemplo producimos un número, y siempre el mismo, realmente no es importante para la explicación
//Simulamos un tiempo random entre 0 y 50 que nos simule un tiempo de producción del producto
Sleep( rand() % 50);
Buffer[(QueueStartOffset + QueueSize) % BUFFER_SIZE] = 1;
QueueSize++;
TotalItemsProducidos++;
printf("Un productor ha producido un producto\n");
LeaveCriticalSection(&BufferLock);
// Si un consumidor esta esperando lo despertamos
WakeConditionVariable(&BufferNotEmpty);
return 0;
}
Lo primero que hacemos es entrar en la sección crítica, si nuestro Buffer está lleno dormimos al hilo porque no tiene espacio para producir más elementos:
while (QueueSize == BUFFER_SIZE)
{
// El buffer esta lleno, dormimos al thread
SleepConditionVariableCS(&BufferNotFull, &BufferLock, INFINITE);
}
En caso de que haya espacio para producir elementos, producimos el elemento:
// Insertamos el elemento al final de la cola e incrementamos el tamaño
// En este ejemplo producimos un número, y siempre el mismo, realmente no es importante para la explicación
//Simulamos un tiempo random entre 0 y 50 que nos simule un tiempo de producción del producto
Sleep( rand() % 50);
Buffer[(QueueStartOffset + QueueSize) % BUFFER_SIZE] = 1;
QueueSize++;
TotalItemsProducidos++;
printf("Un productor ha producido un producto\n");
Por último abandonamos la sección crítica y avisamos a los consumidores que puedan haber dormidos de que ya hay un elemento que pueden consumir:
LeaveCriticalSection(&BufferLock);
// Si un consumidor esta esperando lo despertamos
WakeConditionVariable(&BufferNotEmpty);
El consumidor funciona de la misma forma, simplemente se duerme cuando no hay elementos y después de consumir un elemento avisa a los productores que pueda haber esperando porque el buffer estaba lleno y se durmieron de que ya pueden producir un nuevo elemento:
DWORD WINAPI ThreadConsumidor(PVOID p)
{
// Entramos en la sección crítica
EnterCriticalSection(&BufferLock);
while (QueueSize == 0)
{
// El buffer esta lleno, dormimos al thread
SleepConditionVariableCS(&BufferNotEmpty, &BufferLock, INFINITE);
}
//Nos dormimos un tiempo random entre 0 y 30 para simular lo que nos cuesta consumir un producto
Sleep( rand() % 30);
LONG Item = Buffer[QueueStartOffset];
QueueSize--;
QueueStartOffset++;
TotalItemsConsumidos++;
if (QueueStartOffset == BUFFER_SIZE)
{
QueueStartOffset = 0;
}
printf("Un consumidor ha consumido un producto\n");
LeaveCriticalSection(&BufferLock);
// Si un productor esta esperando lo despertamos
WakeConditionVariable(&BufferNotFull);
return 0;
}
La salida de nuestro programa:
Un productor ha producido un producto
Un productor ha producido un producto
Un productor ha producido un producto
Un productor ha producido un producto
Un productor ha producido un producto
Un consumidor ha consumido un producto
Un productor ha producido un producto
Un consumidor ha consumido un producto
Un productor ha producido un producto
Un consumidor ha consumido un producto
Un productor ha producido un producto
....
....
Un productor ha producido un producto
Un consumidor ha consumido un producto
Un productor ha producido un producto
Un consumidor ha consumido un producto
Un consumidor ha consumido un producto
Un consumidor ha consumido un producto
Un consumidor ha consumido un producto
Un consumidor ha consumido un producto
Numero de items producidos: 150
Numero de items consumidos: 150
Ha terminado el thread y ahora va a terminarse el programa principal!
Creo que ya hemos tenido bastante sobre Threads y la WIN API por hoy. Espero que os haya gustado la entrada y recordad, dejad un comentario y compartir en redes sociales.
Un saludo programadores