Febrero 2005

Anuncio
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 : programacion.concurrente@lsi.uned.es
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);
};
Descargar