INGENIERÍA TÉCNICA en INFORMÁTICA de SISTEMAS ASIGNATURA: PROGRAMACIÓN CONCURRENTE CÓDIGO ASIGNATURA: 403182/533012 MATERIAL AUXILIAR: NINGUNO DURACIÓN: 2 horas Fecha 25 de Enero de 2005 CONTACTO : [email protected] TEORIA Equivalencia de herramientas. Implementar las primitivas de los semáforos a partir de las regiones crı́ticas condicionales. (2.5pt) var c : entero; resource s : c; wait(s) : region s when c > 0 do c := c - 1; signal(s) : region s do c := c + 1; init(s,v) : region s do c := v; Explicar cuales son las ventajas e inconvenientes de los sucesos en relación con las regiones crı́ticas condicionales. (1.5pt) Entre las ventajas de los sucesos respecto a las RCC figura la posibilidad de definir varias colas de eventos (frente a sólo una por recurso), a gusto del programador y realizar una gestión explı́cita de dichas colas, lo que, bien empleado, redunda en una mayor eficiencia, puesto que se pueden seleccionar los procesos que queremos despertar con una granularidad definida por el programador, mientras que con las RCC, cuando un proceso abandona la RC, se despierta a todos los que estaban bloqueados en la cola de eventos para que re-evalúen la condición, con la probable pérdida de eficiencia que ello conlleva en términos de cambios de contexto. Para que se produzca una mejora apreciable de la eficiencia es necesario que exista un alto grado de procesos y de condiciones distintas a evaluar dentro de las regiones crı́ticas, de lo contrario esta ventaja puede convertirse en una desventaja. Entre las desventajas de los sucesos respecto a las RCC, hay que decir que la gestión explı́cita de las colas puede introducir una sobrecarga perjudicial para la eficiencia cuando hay muchas colas y pocos procesos, o bien cuando hay pocas condiciones distintas a considerar. Por último, pero no menos importante, hay que destacar que las RCC son estupendas para reflejar condiciones de sincronización, pero los sucesos lo son mucho menos porque después de hacer un CAUSE, un proceso sigue en ejecución, lo que hace que para cuando un proceso salga de la cola del evento, es posible que la condición que necesitaba haya dejado de cumplirse, lo que obliga a encerrar los AWAIT en estructuras del estilo while not Condición do AWAIT(ev); o a hacer un diseño muy cuidadoso de las modificaciones de las condiciones dentro de las regiones crı́ticas. EJERCICIO Un banco tiene repartidos diversos cajeros automáticos repartidos por la ciudad. Las operaciones que se pueden hacer desde un cajero son consultar el saldo y sacar dinero. Queremos simular el comportamiento de los cajeros de la siguiente manera. Escribir un programa en pseudocódigo en el que varios procesos cajero vayan realizando aleatoriamente estas operaciones. Es decir, dentro de un bucle infinito, un proceso cajero tiene que decidir aleatoriamente si realiza una consulta o una disposición de efectivo, decidir aleatoriamente el número de cuenta y, en caso de tratarse de una disposición de efectivo, la cantidad. El grado de concurrencia entre los procesos debe ser alto. Varios cajeros pueden querer acceder o modificar los datos de una cuenta al mismo tiempo. Habrá que asegurar la consistencia en los datos. Además, los cajeros denegarán las disposiciones de efectivo si dentro de las últimas 24 horas se han retirado más de 500 euros. Para poder seguir lo que está ocurriendo es menester que los procesos emitan mensajes por pantalla de vez en cuando para indicar lo que está pasando, por ejemplo: ’Cajero xxx: Quiero realizar una disposición de 124 euros de la cuenta 27’, ’Cajero yyy: Operación no permitida, lı́mite de disposición alcanzado’ etc... (6pt). Se trata de SIMULAR, como bien dice el enunciado, por lo que una solución basada en memoria compartida es perfectamente aplicable. Es un problema de libro de lectores y escritores. Muchos lectores pueden leer concurrentemente de una misma cuenta, pero sólo uno puede escribir. En el enunciado se indica claramente que la concurrencia es muy importante. Imaginemos una red bancaria donde cuando un usuario introduce una tarjeta en un cajero, todos los demás quedan bloqueados. Se trata de una situación absolutamente inaceptable. Una situación mejor será aquella en la que cuando se introduce una tarjeta, se queda bloqueada sólo la cuenta correspondiente. Aún ası́, no es una solución del todo correcta. Podemos pensar en tarjetas de empresa, que tienen multitud de tarjetas fı́sicas asociadas a la misma cuenta, o a tarjetas familiares, situación bastante habitual. El proceso para realizar correctamente la pregunta deberı́a incluir ciertos pasos. En primer lugar debemos decidir nuestra estructura de procesos y paso de información. Aquı́ optaremos por un único tipo de proceo cajero, del cual habrá muchas materalizaciones independientes. En cuanto a la información, utilizaremos una memoria compartida con los datos de las cuentas. Serı́a recomendable establecer explı́citamente cuáles son los requerimientos de concurrencia y las condiciones de sincronización entre estos procesos cajero. Requerimientos de concurrencia. 1. Varios procesos cajero pueden querer realizar consultas al mismo tiempo sobre la memorı́a compartida. Esto debe ser permitido. 2. Más concretamente, se debe permitir que varios procesos cajero consulten la misma cuenta de forma concurrente. 3. Las consultas a los datos DEBEN hacerse en zonas concurrentes. (e.d. no en exclusión mutua). Condiciones de sincronización 1. Si hay un proceso modificando los datos de una cuenta no puede haber ni lectores ni otros escritores activos sobre dicha cuenta. 2. Puede haber cajeros modificando concurrentemente las cuentas siempre y cuando cada uno modique una cuenta distinta. Otros condicionamientos previos al correcto desarrollo del programa en pseudocódigo serı́an: 1. Deben existir las estructuras de datos adecuadas (en este caso una tabla con las cuentas, que deben incluir el saldo y una lista con las últimas extracciones y su fecha y hora). 2. Deben diseñarse mensajes informativos apropiados. Este último punto es fundamental. Puesto que se trata de llevar a cabo una simulación, podemos imaginar el proceso visto desde un observador como una especie de caja negra, de la que lo único que sale son los mensajes informativos. Si los procesos no explican con claridad lo que están haciendo, la simulación resulta decepcionante; no se puede apreciar con claridad la concurrencia, ni tampoco las condiciones de sincronización. Después de estas consideraciones, podemos concretar un poco más. Se trata del problema clásico de los lectores y escritores. Esto no deberı́a resultar muy sorprendente puesto que en programación concurrente hay tres problemas clásicos: el problema de los lectores y escritores, el problema de los productores y consumidores y el problema de los filósofos (cuyo valor es más pedagógico que práctico). De modo que tenemos dos problemas clásicos y este es uno de ellos. En el libro de texto de la asignatura (Palma et al.) podemos encontrar el problema de los lectores y escritores resuelto con: semáforos, regiones crı́ticas condicionales, monitores, buzones, canales y mediante invocación remota. No hay pues falta de alternativas. El único refinamiento a tener en cuenta para este problema es que se trata de un problema de lectores y escritores generalizado, puesto que hay uno por cada cuenta. Los semáforos son una herramienta de bajo nivel y poco recomendable para problemas complejos, sin embargo, al tratarse de un problema ampliamente estudiado es perfectamente aceptable el utilizarlos. Además, se trata de una solución que escala muy bien a tener vectores de semáforos, lo que podrı́a ser complicado con otras herramientas como los monitores. Hechas todas estas precisiones, podemos pasar escribir el pseudocódigo correspondiente. Hemos escogido la solución con prioridad para los escritores, puesto que lo más habitual es que los cajeros se utilicen para sacar dinero con más frecuencia que para consultar. En lugar de dar la solución en pseudocódigo, vamos a dar un programa escrito en C con ayuda de la librerı́a POSIX Threads. // Compilado en SuSE Linux 9.2 con // gcc -O3 -o programa_cajeros programa_cajeros.c -lpthread #include <stdlib.h> #include <pthread.h> #define #define #define #define #define #define MAXCAJEROS 500 MAXCUENTAS 10000 MAXDISP 500 TOPE 1000000 TIEMPO_LIMITE 10 ESPERA_ALEATORIA 20 enum t_operacion {consulta, disposicion}; typedef struct { int tiempo, cantidad; } t_extracciones; typedef struct { t_extracciones entrada; struct lista* siguiente; } lista; typedef struct { int saldo; lista* extracciones; } entrada_cuenta; entrada_cuenta cuentas[MAXCUENTAS]; int nl[MAXCUENTAS], nle[MAXCUENTAS], nee[MAXCUENTAS]; pthread_t hilos[MAXCAJEROS]; pthread_mutex_t mutex[MAXCUENTAS], lector[MAXCUENTAS], escritor[MAXCUENTAS]; int escribiendo[MAXCUENTAS]; int i,j; int protocolo_entrada_lectura(int no_cuenta, int no_cajero) { printf("Cajero %3d : Empiezo a leer de la cuenta %d\n", no_cajero, no_cuenta); pthread_mutex_lock (&mutex[no_cuenta]); // Si se esta escribiendo o existen escritores en espera // el lector debe ser bloqueado if (escribiendo[no_cuenta] > 0 || nee[no_cuenta] > 0) { nle[no_cuenta] ++; pthread_mutex_unlock (&mutex[no_cuenta]); pthread_mutex_lock (&lector[no_cuenta]); nle[no_cuenta]--; } nl[no_cuenta]++; if (nle[no_cuenta] > 0) {// Desbloqueo encadenado pthread_mutex_unlock (&lector[no_cuenta]); } else { pthread_mutex_unlock (&mutex[no_cuenta]); } return 0; } int protocolo_salida_lectura(int no_cuenta, int no_cajero) { pthread_mutex_lock (&mutex[no_cuenta]); nl[no_cuenta]--; // Desbloquear un escritor si es posible if (nl[no_cuenta] == 0 && nee[no_cuenta] > 0) { pthread_mutex_unlock(&escritor[no_cuenta]); } else { pthread_mutex_unlock(&mutex[no_cuenta]); } printf("Cajero %3d : Termino de leer de la cuenta %d\n", no_cajero, no_cuenta); } int protocolo_entrada_escritura(int no_cuenta, int no_cajero) { printf("Cajero %3d : Empiezo a escribir en la cuenta %d\n", no_cajero, no_cuenta); pthread_mutex_lock(&mutex[no_cuenta]); // Si se esta escribiendo o existen lectores // el escritor debe ser bloqueado if (nl[no_cuenta] > 0 || escribiendo[no_cuenta] > 0) { nee[no_cuenta]++; pthread_mutex_unlock(&mutex[no_cuenta]); pthread_mutex_lock(&escritor[no_cuenta]); nee[no_cuenta] --; } escribiendo[no_cuenta] = 1; pthread_mutex_unlock(&mutex[no_cuenta]); } int protocolo_salida_escritura(int no_cuenta, int no_cajero) { pthread_mutex_lock(&mutex[no_cuenta]); // Esto no viene en el libro escribiendo[no_cuenta] = 0; // En el libro pone ne := ne -1 // Desbloquear un escritor que este a la espera // Y si no hay, desbloquear a un lector que este a la espera if (nee[no_cuenta] > 0) { pthread_mutex_unlock(&escritor[no_cuenta]); } else if (nle[no_cuenta] > 0) { pthread_mutex_unlock(&lector[no_cuenta]); } else { pthread_mutex_unlock(&mutex[no_cuenta]); } printf("Cajero %3d : Termino de escribir en la cuenta %d\n", no_cajero, no_cuenta); } void * proceso_cajero(void *p) { int id = (int) p; enum t_operacion operacion; unsigned cantidad, cuenta, fechayhora; int total24h = 0; lista* recorre = NULL; t_extracciones miextraccion; lista* ext = NULL; lista* aux = NULL; int ahora = 0; int espera = 0; for(;;) { total24h = 0; cantidad = 0; // Elegir numero de cuenta y operacion cuenta = (int) (MAXCUENTAS * (float) rand() / (RAND_MAX +1.0)); operacion = (int) (2 * (float) rand() / (RAND_MAX + 1.0)); if (operacion == disposicion) { cantidad = (int) (TOPE * (float) rand() / (RAND_MAX + 1.0)) + 1; printf("Cajero %3d : Quiero disponer %d euros de la cuenta %d\n", id, cantidad, cuenta); protocolo_entrada_escritura(cuenta, id); ahora = time(NULL); for(ext = cuentas[cuenta].extracciones ; ext != NULL && (ext->entrada.tiempo - ahora < TIEMPO_LIMITE); ext = ext->siguiente) { total24h += ext->entrada.cantidad; } for(; ext != NULL; ext = ext->siguiente) { aux = ext->siguiente; free(aux); } if (cuentas[cuenta].saldo < cantidad) printf("Cajero %3d : Saldo insuficiente. Saldo actual %d, importe %d\n", id, cuentas[cuenta].saldo, cantidad); else if (total24h > MAXDISP) printf("Cajero %3d : Se ha superado el lı́mite de disposicion para el periodo de tiempo\n", id); else { cuentas[cuenta].saldo -= cantidad; printf("Cajero %3d : Operación realizada correctamente. Saldo resultante %d\n", id, cuentas[cuenta].saldo); miextraccion.tiempo = time(NULL); miextraccion.cantidad = cantidad; lista* p = (lista *) malloc(sizeof(lista)); p->entrada = miextraccion; p->siguiente = cuentas[cuenta].extracciones; cuentas[cuenta].extracciones = p; }; protocolo_salida_escritura(cuenta, id); espera = (int) (ESPERA_ALEATORIA * (float) rand() / (RAND_MAX +1.0)); sleep(espera); } else if (operacion == consulta) { printf("Cajero %3d : Quiero realizar una consulta de saldo sobre la cuenta %d\n", id, cuenta); protocolo_entrada_lectura(cuenta, id); printf("Cajero %3d : El saldo de la cuenta %d asciende a %d euros\n", id, cuenta, cuentas[cuenta].saldo); protocolo_salida_lectura(cuenta, id); espera = (int) (ESPERA_ALEATORIA * (float) rand() / (RAND_MAX +1.0)); sleep(espera); } else { printf("Operación desconocida\n"); exit(-1); } } } int main(int argc, char* argv[]) { int rc; printf("Principio de la ejecucion del programa\n"); for(i = 0; i < MAXCUENTAS; i++) { cuentas[i].saldo = (int) (TOPE * (float) rand() / (RAND_MAX +1.0)); cuentas[i].extracciones = NULL; printf("El saldo inicial de la cuenta %d es de %d euros\n", i, cuentas[i].saldo); } for(j = 0; j < MAXCAJEROS; j++) { printf("Creando cajero %d\n", j); rc = pthread_create(&hilos[j], NULL, proceso_cajero, (void *)j); if (rc) { printf("ERROR; el codigo de salida de pthread_create() es %d\n", rc); exit(-1); } rc = pthread_mutex_init (&mutex[j], NULL); if (rc) { printf("ERROR; el codigo de salida de pthread_mutex_init() es %d\n", rc); exit(-1); } } pthread_join(hilos[0], NULL); };