Nuestro código comienza en el main. Lo primero que hacemos es comprobar si el nº
de argumentos introducido es correcto. Si es así, guardamos dichos valores en las
variables file_name, num_producers, num_consumers y buff_size. Para guardar los
valores en estas 3 últimas variables, hacemos uso de atoi (convierte una cadena de
caracteres a un nº entero. Si no puede devuelve 0). Esto nos servirá para el control de
errores que hacemos a continuación. Si todos los argumentos introducidos son
correctos, pasamos a la siguiente fase, leer el fichero de entrada para ver el nº de
operaciones que hay.
Declaramos un array dinámico (buffer). Abrimos el fichero de entrada con open. Vamos leyendo dicho archivo y guardando los caracteres en buffer con read. Al llegar al final de la primera línea, guardamos el valor en num_max_operaciones. Continuamos leyendo el fichero. Cada vez que llegamos a un salto de línea, aumentamos el valor de num_operaciones. Una vez terminamos de leer el fichero, hacemos una comparación entre num_max_operaciones y num_operaciones para ver si este segundo valor no es menor que el primero. Si no es menor, el próximo paso es guardar las líneas del fichero en memoria para su posterior uso.
Abrimos nuevamente el fichero, esta vez con fopen. Creamos un array dinámico de tipo element, reservando memoria para num_max_operaciones elementos llamado array_operaciones . Con fgets, las líneas del fichero se van leyendo y almacenando en buff. NOTA: La 1ª línea no nos interesa. Contiene el nº de operaciones del fichero, que es un dato que ya tenemos. Mediante un bucle for que se repite num_max_operaciones veces vamos a ir leyendo y procesando las líneas del fichero. Para ello, haremos uso de una función auxiliar llamada almacenar_operaciones. Esta recibe el id del producto, el coste de compra, el precio de venta, el array dónde se almacenará la información, la línea leída del fichero, el nº de línea del fichero y un nº que indica la posición hasta la que se ha leído de la línea.
El primer dato que nos encontramos al leer una línea es el id del producto. Dependiendo de este, se le pasará a almacenar_operaciones un coste de compra y precio de venta determinado. Dentro de la función auxiliar, guardamos en array_operaciones los primeros datos disponibles, product_id, coste_de_compra y precio_de_venta. El próximo dato a leer es el tipo de operación. Para ello avanzamos la posición en uno y utilizamos sscanf. Dependiendo del dato leído, guardaremos 0 en caso de PURCHASE o 1 en caso de SALE. En caso de leer otra cosa saldrá error. Por último, queda leer el nº de unidades. Para ello, avanzamos hasta la posición previa de dicho dato, usamos sscanf para extraer la información y la guardamos en array_operaciones.
Utilizamos atoi ya que queremos que se guarde un número entero. Una vez que tenemos la información del fichero guardada en array_operaciones, podemos pasar a la concurrencia.
Inicializamos la cola circular, los hilos, los mutex y las variables de condición. Estos 3 últimos han sido declarados al inicio del código. Lo primero que se hace es la creación de los hilos productores. Como se busca hacer un reparto de las operaciones entre los distintos hilos productores, hemos creado un array p_arg de tipo producer_arg para que cada hilo ejecute un nº de operaciones determinado. producer_arg se trata de un struct que contiene array_operaciones, la posición de la primera operación a ejecutar y la posición de la última operación a ejecutar.
Antes de entrar en el bucle en el que se crean los hilos productores, calculamos el nº de operaciones que realizará cada uno de ellos. También se calcula el resto por si el nº de productores es impar. Dentro del bucle, se asigna la información necesaria a p_arg, que será pasado como parámetro en la creación del hilo con pthread_create. Se llama a productor.
Para los hilos consumidores hacemos algo parecido. Queremos que se repartan el nº de operaciones a consumir entre los distintos consumidores. Para ello, calculamos el nº de operaciones que realizará cada uno de ellos. Calculamos el resto por si el nº de consumidores es impar. Dentro del bucle creamos num_op, que contendrá el nº de operaciones a consumir. Se le pasa a pthread_create como parámetro. Se llama a consumidor.
Productor: Es la función que ejecutan los hilos productores. Cada uno de ellos ejecutará el nº de operaciones que le corresponda. Dentro del bucle for nos encontramos una sección crítica en la que pueden producirse condiciones de carrera (varios hilos pueden acceder a la cola circular y modificarla). Es por eso por lo que utilizamos pthread_mutex_lock(&mutex) y pthread_mutex_unlock(&mutex). Si seguimos, nos encontramos una variable de condición que se está ejecutando continuamente. Esta sirve para comprobar si la cola está o no llena. Si está llena, &mutex quedará bloqueado hasta que no reciba la condición &no_lleno. Si hemos llegado hasta aquí, significa que la cola no está llena, por lo que podremos insertar un elemento. Acto seguido hacemos un pthread_cond_signal(&no_vacio) para indicar que la cola no está vacía y un pthread_mutex_unlock(&mutex) para liberar &mutex y permitir que otros hilos accedan a la sección crítica.
Consumidor: Es la función que ejecutan los hilos consumidores. Cada uno de ellos ejecutará el nº de operaciones que le corresponda. Dentro del bucle for nos encontramos una sección crítica en la que pueden producirse condiciones de carrera (varios hilos pueden acceder a la cola circular y modificarla). Es por eso por lo que utilizamos pthread_mutex_lock(&mutex) y pthread_mutex_unlock(&mutex).
Si seguimos, nos encontramos una variable de condición que se está ejecutando continuamente. Esta sirve para comprobar si la cola está o no vacía. Si está vacía, &mutex quedará bloqueado hasta que no reciba la condición &no_vacia. Si hemos llegado hasta aquí, significa que la cola no está vacía, por lo que podremos extraer un elemento. Acto seguido ejecutamos la función calculos, la cual recibe el elemento extraído de la cola para calcular el stock restante y el beneficio obtenido. Por último, hacemos un pthread_cond_signal(&no_lleno) para indicar que la cola no está llena y un pthread_mutex_unlock(&mutex) para liberar &mutex y permitir que otros hilos accedan a la sección crítica.
Mientras tanto, hay un pthread_join ejecutándose esperando la terminación de los hilos. Por último, se liberan los recursos del programa y se imprimen los resultados calculados. Contiene las funciones de la cola. Podemos encontrar:
→ queue_init: Crea una nueva cola vacía con una capacidad dada y devuelve un puntero a ella.
→ queue_destroy: Destruye una cola existente liberando la memoria que reservó para ella.
→ queue_put: Añade un elemento al final de la cola, actualizando su índice y su tamaño.
→ queue_get: Extrae el primer elemento de la cola, actualizando su índice y su tamaño. Devuelve el elemento extraído.
→ queue_empty: Comprueba si la cola está o no vacía. Devuelve 1 en caso de estarlo y 0 en caso contrario.
→ queue_full: Comprueba si la cola está o no llena. Devuelve 1 en caso de estarlo y 0 en caso contrario. Definimos tanto la estructura de un elemento como la estructura de la cola.
→ element: Está formado por el id del producto (con un coste de compra y precio de venta asociado), el tipo de operación y el nº de unidades.
→ queue: Tiene un índice que apunta al primer elemento de la cola (head), un índice que apunta al último elemento (tail), el nº actual de elementos en la cola (size), la capacidad máxima de la cola (capacity) y un puntero a la memoria donde se almacenan los elementos de la cola (data).
Declaramos un array dinámico (buffer). Abrimos el fichero de entrada con open. Vamos leyendo dicho archivo y guardando los caracteres en buffer con read. Al llegar al final de la primera línea, guardamos el valor en num_max_operaciones. Continuamos leyendo el fichero. Cada vez que llegamos a un salto de línea, aumentamos el valor de num_operaciones. Una vez terminamos de leer el fichero, hacemos una comparación entre num_max_operaciones y num_operaciones para ver si este segundo valor no es menor que el primero. Si no es menor, el próximo paso es guardar las líneas del fichero en memoria para su posterior uso.
Abrimos nuevamente el fichero, esta vez con fopen. Creamos un array dinámico de tipo element, reservando memoria para num_max_operaciones elementos llamado array_operaciones . Con fgets, las líneas del fichero se van leyendo y almacenando en buff. NOTA: La 1ª línea no nos interesa. Contiene el nº de operaciones del fichero, que es un dato que ya tenemos. Mediante un bucle for que se repite num_max_operaciones veces vamos a ir leyendo y procesando las líneas del fichero. Para ello, haremos uso de una función auxiliar llamada almacenar_operaciones. Esta recibe el id del producto, el coste de compra, el precio de venta, el array dónde se almacenará la información, la línea leída del fichero, el nº de línea del fichero y un nº que indica la posición hasta la que se ha leído de la línea.
El primer dato que nos encontramos al leer una línea es el id del producto. Dependiendo de este, se le pasará a almacenar_operaciones un coste de compra y precio de venta determinado. Dentro de la función auxiliar, guardamos en array_operaciones los primeros datos disponibles, product_id, coste_de_compra y precio_de_venta. El próximo dato a leer es el tipo de operación. Para ello avanzamos la posición en uno y utilizamos sscanf. Dependiendo del dato leído, guardaremos 0 en caso de PURCHASE o 1 en caso de SALE. En caso de leer otra cosa saldrá error. Por último, queda leer el nº de unidades. Para ello, avanzamos hasta la posición previa de dicho dato, usamos sscanf para extraer la información y la guardamos en array_operaciones.
Utilizamos atoi ya que queremos que se guarde un número entero. Una vez que tenemos la información del fichero guardada en array_operaciones, podemos pasar a la concurrencia.
Inicializamos la cola circular, los hilos, los mutex y las variables de condición. Estos 3 últimos han sido declarados al inicio del código. Lo primero que se hace es la creación de los hilos productores. Como se busca hacer un reparto de las operaciones entre los distintos hilos productores, hemos creado un array p_arg de tipo producer_arg para que cada hilo ejecute un nº de operaciones determinado. producer_arg se trata de un struct que contiene array_operaciones, la posición de la primera operación a ejecutar y la posición de la última operación a ejecutar.
Antes de entrar en el bucle en el que se crean los hilos productores, calculamos el nº de operaciones que realizará cada uno de ellos. También se calcula el resto por si el nº de productores es impar. Dentro del bucle, se asigna la información necesaria a p_arg, que será pasado como parámetro en la creación del hilo con pthread_create. Se llama a productor.
Para los hilos consumidores hacemos algo parecido. Queremos que se repartan el nº de operaciones a consumir entre los distintos consumidores. Para ello, calculamos el nº de operaciones que realizará cada uno de ellos. Calculamos el resto por si el nº de consumidores es impar. Dentro del bucle creamos num_op, que contendrá el nº de operaciones a consumir. Se le pasa a pthread_create como parámetro. Se llama a consumidor.
Productor: Es la función que ejecutan los hilos productores. Cada uno de ellos ejecutará el nº de operaciones que le corresponda. Dentro del bucle for nos encontramos una sección crítica en la que pueden producirse condiciones de carrera (varios hilos pueden acceder a la cola circular y modificarla). Es por eso por lo que utilizamos pthread_mutex_lock(&mutex) y pthread_mutex_unlock(&mutex). Si seguimos, nos encontramos una variable de condición que se está ejecutando continuamente. Esta sirve para comprobar si la cola está o no llena. Si está llena, &mutex quedará bloqueado hasta que no reciba la condición &no_lleno. Si hemos llegado hasta aquí, significa que la cola no está llena, por lo que podremos insertar un elemento. Acto seguido hacemos un pthread_cond_signal(&no_vacio) para indicar que la cola no está vacía y un pthread_mutex_unlock(&mutex) para liberar &mutex y permitir que otros hilos accedan a la sección crítica.
Consumidor: Es la función que ejecutan los hilos consumidores. Cada uno de ellos ejecutará el nº de operaciones que le corresponda. Dentro del bucle for nos encontramos una sección crítica en la que pueden producirse condiciones de carrera (varios hilos pueden acceder a la cola circular y modificarla). Es por eso por lo que utilizamos pthread_mutex_lock(&mutex) y pthread_mutex_unlock(&mutex).
Si seguimos, nos encontramos una variable de condición que se está ejecutando continuamente. Esta sirve para comprobar si la cola está o no vacía. Si está vacía, &mutex quedará bloqueado hasta que no reciba la condición &no_vacia. Si hemos llegado hasta aquí, significa que la cola no está vacía, por lo que podremos extraer un elemento. Acto seguido ejecutamos la función calculos, la cual recibe el elemento extraído de la cola para calcular el stock restante y el beneficio obtenido. Por último, hacemos un pthread_cond_signal(&no_lleno) para indicar que la cola no está llena y un pthread_mutex_unlock(&mutex) para liberar &mutex y permitir que otros hilos accedan a la sección crítica.
Mientras tanto, hay un pthread_join ejecutándose esperando la terminación de los hilos. Por último, se liberan los recursos del programa y se imprimen los resultados calculados. Contiene las funciones de la cola. Podemos encontrar:
→ queue_init: Crea una nueva cola vacía con una capacidad dada y devuelve un puntero a ella.
→ queue_destroy: Destruye una cola existente liberando la memoria que reservó para ella.
→ queue_put: Añade un elemento al final de la cola, actualizando su índice y su tamaño.
→ queue_get: Extrae el primer elemento de la cola, actualizando su índice y su tamaño. Devuelve el elemento extraído.
→ queue_empty: Comprueba si la cola está o no vacía. Devuelve 1 en caso de estarlo y 0 en caso contrario.
→ queue_full: Comprueba si la cola está o no llena. Devuelve 1 en caso de estarlo y 0 en caso contrario. Definimos tanto la estructura de un elemento como la estructura de la cola.
→ element: Está formado por el id del producto (con un coste de compra y precio de venta asociado), el tipo de operación y el nº de unidades.
→ queue: Tiene un índice que apunta al primer elemento de la cola (head), un índice que apunta al último elemento (tail), el nº actual de elementos en la cola (size), la capacidad máxima de la cola (capacity) y un puntero a la memoria donde se almacenan los elementos de la cola (data).
