Programación Concurrente: Septiembre 2007 David Fernández-Amorós [email protected] El juego del laberinto concurrente se juega en un tablero rectangular de n filas y m columnas. En cada casilla puede haber una pared, un espacio vacío o un coco. El objetivo de cada coco es salir del laberinto. Cada casilla tiene como máximo ocho posiciones adyacentes (incluyendo las diagonales). Un coco se va moviendo dentro del tablero, siempre que tenga alguna casilla adyacente libre, hasta que consigue llegar a una de las casillas del rectángulo más externo, que se consideran la salida del laberinto. El desplazamiento sólo es posible a una casilla vacía. Cada coco mantiene una lista de las casillas por las que ha pasado. La siguiente casilla para un coco se calcula de la siguiente manera: Se analizan las direcciones de desplazamiento en el orden de las agujas del reloj, empezando por la dirección arriba y a la izquierda. Si la posición actual es (x,y), entonces la primera dirección es la que lleva a (x-1, y-1), después, la de (x,y-1), a continuación la de (x+1,y-1) y así sucesivamente. La primera dirección que proporcione una casilla libre y que no haya sido visitada se utiliza para moverse. Si todas las casillas vecinas han sido visitadas, entonces se levanta esta restricción y el coco se mueve en la primera dirección, según el orden explicado, en la que haya una casilla libre. Si no se encuentra ninguna casilla libre adyacente, el proceso de evaluación de casillas vuelve a empezar. Las casillas adyacentes no pueden cambiar mientras dura el proceso de evaluación de la situación, pero otros cocos pueden examinar el contenido de esas mismas casillas. Evidentemente, dos cocos distintos no pueden moverse al mismo tiempo hacia la misma casilla. Se pide simular el juego del laberinto concurrente mediante un programa concurrente escrito en pseudocódigo utilizando semáforos. Utilizar un único tipo de proceso: coco. Para seguir la simulación, los cocos deben emitir unos mensajes por pantalla. Cada mensaje debe incluir el número de coco, la posición que ocupa y las posiciones de las casillas que ha visitado. Cada coco debe emitir un mensaje indicando en qué posición del tablero empieza. Esta posición inicial se debe elegir aleatoriamente entre las casillas vacías, o bien al iniciarse el proceso, o bien en el programa principal. También deben emitirse mensajes cada vez que un coco decida moverse, especificando de dónde a dónde se produce el movimiento. Los cocos también deben emitir un mensaje antes de salir del laberinto. La simulación debe contar con unas estructuras de datos que permitan resolver razonablemente el problema. Además, se prestará atención para evitar las situaciones de interbloqueo, inanición y espera activa. Se debe procurar el máximo nivel de concurrencia entre los procesos. Si se identifica un problema-tipo (productores/consumidores o lectores/escritores) debe mencionarse explícitamente (incluyendo, en su caso, la prioridad más adecuada) antes de proceder con el pseudocódigo. Es muy importante que el comportamiento de los cocos se ajuste al enunciado, tanto en los aspectos específicamente concurrentes como en los demás. Es muy importante leer con atención el enunciado para responder adecuadamente a lo que se nos pide. En principio, la forma óptima de simular el comportamiento que nos piden dependería principalmente de la proporción de paredes frente a casillas vacías y a la 1 Programación Concurrente: Septiembre 2007 David Fernández-Amorós [email protected] proporción de número de cocos frente a número de casillas vacías. Un programa que fuera muy eficiente en un tablero con muchos cocos y pocas casillas libres, donde podría haber varios cocos mirando la misma casilla, no lo sería en otro contexto donde hubiera pocos cocos, y por tanto, pocas posibilidades de que dos cocos quisieran leer la misma casilla al mismo tiempo. Afortunadamente, no tenemos que darle muchas vueltas a esta cuestión: el enunciado nos dice que debe ser posible que varios cocos evalúen la misma casilla. Esto encaja a la perfección con uno de los dos problemas-tipo de la asignatura: lectores y escritores. El esquema de los lectores y escritores es, en la práctica, la única manera en la que varios procesos pueden leer un recurso concurrentemente. Siempre que no se trate de un recurso de sólo lectura, es decir, de un constante. Si es posible que los procesos puedan leer y puedan escribir en un recurso, entonces o bien se bloquea el recurso antes de acceder a el, o bien tenemos un esquema de lectores y escritores, en el que existen unos protocolos de entrada a la lectura y de entrada a la escritura que preparan el acceso al recurso. Un problema bastante frecuente es el de leer variables compartidas sin pedir exclusión mutua. Esto es un error porque como hemos señalado en el párrafo anterior, si hay un proceso que puede modificar la variable, no podremos saber que el valor que hemos leído es correcto, tampoco podremos saber si ha cambiado desde el momento de la lectura. Otra cuestión interesante es la de la granularidad. Una posibilidad, un tanto simplista, es considerar el tablero como un único recurso. Otra posibilidad que aumenta considerablemente la concurrencia es considerar que cada casilla es un recurso distinto. Lo bueno de este enfoque es que no es mucho más complicado. Se trata de aplicar el esquema lectores y escritores a cada casilla. En este caso, hemos elegido la prioridad para la escritura, con el fin de evitar la inanición, aunque también hay argumentos a favor de la prioridad para la lectura. program laberinto de cocos; const N = 10; M = 15; Ncocos = 10; // Arrays para calcular facilmente las casillas adyacentes vecinas_x = {-1,0,1,1,1,0,-1,-1}; vecinas_y = {-1, -1, -1, 0, 1, 1, 1, 0}; var mutex_mensaje : semaphore; // Las variables del modelo de lectores y escritores mutex, lector, escritor : array [1..N, 1..M] of semaphore; nl, nle, nee : array [1..N, 1..M] of integer; escribiendo : array[1..N, 1..M] of boolean; // Variables para indices de bucles; i,j : integer; // Para guardar las posiciones iniciales de los cocos, // no se usan fueran del programa principal coordenada_x, coordenada_y : array[1..Ncocos] of integer; tablero : array[1..N][1..M] of { vacia, pared, uncoco } 2 Programación Concurrente: Septiembre 2007 David Fernández-Amorós [email protected] procedure entrada_lectura(x,y : integer); begin wait(mutex[x,y]); // Si se está escribiendo o existen escritores en espera // el lector debe ser bloqueado if (escribiendo[x,y] or nee[x,y] > 0) then begin nle[x,y] := nle[x,y] + 1; signal(mutex[x,y]); wait(lector[x,y]) nle[x,y] := nle[x,y] - 1; end nl[x,y] := nl[x,y] + 1; if (nle[x,y] > 0) then // Desbloqueo encadenado signal(lector[x,y]) else signal(mutex[x,y]) end; // entrada_lectura procedure salida_lectura(x,y : integer); begin wait(mutex[x,y]); nl[x,y] := nl[x,y] - 1; // Desbloquear un escritor si es posible if (nl[x,y] = 0 and nee[x,y] > 0) then signal(escritor[x,y]); else signal(mutex[x,y]); end;// salida_lectura procedure entrada_escritura(x,y : integer); begin wait(mutex[x,y]); // Si se está escribiendo o existen lectores el escritor // debe ser bloqueado if (nl[x,y] > 0 or escribiendo[x,y]) then begin nee[x,y] := nee[x,y]+ 1; signal(mutex[x,y]); wait(escritor[x,y]); nee[x,y] := nee[x,y]- 1; end escribiendo[x,y] = true; signal(mutex[x,y]) end; // entrada_escritura procedure cierra_escritura(x,y : integer); begin wait(mutex[x,y]); escribiendo[x,y] = false; // Desbloquear un escritor que esté a la espera. // Si no, desbloquear un lector en espera if (nee[x,y] > 0) then signal(escritor[x,y]); else if (nle[x,y] > 0) then signal(lector[x,y]); else signal(mutex[x,y]); end // cierra_escritura process type coco(id : integer, coor_x, coor_y : integer); var l,k : integer; // Variables para bucles // La tabla es para uso interno del proceso, //la lista es para imprimir las casillas por las que paso tabla_visitadas : array[1..N][1..M] of boolean; lista_visitadas : array[1..MaxLon] of record x,y : integer end; lon_visitadas : integer; // Longitud de la lista de visitadas // En la primera vuelta de inspección de las casillas adyacentes importa // si las hemos visitado ya o no, en la segunda vuelta no importa_visitadas : boolean; salir_de_bucle : boolean; // Nos dice si ya nos hemos movido de casilla o no estoy_mirando_casilla_adyacente_numero : integer; coordenada_x, coordenada_y, candidata_x, candidata_y : integer; 3 Programación Concurrente: Septiembre 2007 David Fernández-Amorós [email protected] procedure escribir_lista_visitadas; var k : integer; begin for k := 1 to lon_visitadas do write('(', lista_visitadas.x[k], ','. lista_visitadas.y[k], ') '); writeln; end; // escribir_lista_visitadas begin coordenada_x := coor_x; coordenada_y := coor_y; for l:= 1 to N do for k:= 1 to M do tabla_visitadas = false; lon_visitadas = 1; // Primer mensaje wait(mutex_mensaje); writeln(’Hola, soy el coco número ',id,’ y he comenzado en la posición (',coordenada_x, ’,’,coordenada_y,’)’); signal(mutex_mensaje); tabla_visitadas[coordenada_x, coordenada_y] := true; lista_visitadas[lon_visitadas].x := coordenada_x; lista_visitadas[lon_visitadas].y := coordenada_y; // Mientras no estemos en el rectángulo del borde, es decir, en posición de terminar... while coordenada_x > 1 and coordenada_x < N and coordenada_y > 1 and coordenada_y < M) begin importa_visitadas := true; salir_del_bucle := false; while not salir_del_bucle begin estoy_mirando_casilla_adyacente_numero := 1; while estoy_mirando_casilla_adyacente_numero < 9 and not salir_de_bucle begin candidata_x := coordenada_x + vecinas_x[estoy_mirando_casilla_adyacente_numero]; candidata_y := coordenada_y + vecinas_y[estoy_mirando_casilla_adyacente_numero]; if (importa_visitadas and tabla_visitadas[candidata_x][candidata_y] = false) or not importa_visitadas then begin // Miramos si la casilla candidata esta vacia entrada_lectura(candidata_x, candidata_y); if (tablero[candidata_x, candidata_y] = vacia) then begin salida_lectura(candidata_x, candidata_y); // Pedimos bloqueo de escritura sobre la candidata // y sobre la casilla actual entrada_escritura(candidata_x,candidata_y); 4 Programación Concurrente: Septiembre 2007 David Fernández-Amorós [email protected] entrada_escritura(coordenada_x,coordenada_y); // Hay que darse cuenta de que despues de soltar el bloqueo // de lectura, otro coco puede haberse movido a la casilla, // por lo que nos toca comprobar si sigue vacía despues de // obtener el bloqueo de escritura. // Si la casilla candidata sigue vacia... if (tablero[candidata_x, candidata_y] = vacia) then begin tablero[candidata_x, candidata_y] := uncoco; tablero[coordenada_x, coordenada_y] := vacia; salida_escritura(coordenada_x, coordenada_y); salida_escritura(candidata_x, candidata_y); wait(mutex_mensaje); write(’Soy el coco ’,id,’ y me muevo de la casilla (’,coordenada_x, ’,’, coordenada_y,’) hasta la (’, candidata_x,’,’, candidata_y, ’) y he pasado ya por:’); escribir_lista_visitadas; signal(mutex_mensaje); coordenada_x := candidata_x; coordenada_y := candidata_y; tablas_visitadas[coordenada_x, coordenada_y] := true; lon_visitadas := lon_visitadas + 1; lista_visitadas[lon_visitadas].x := coordenada_x; lista_visitadas[lon_visitadas].y := coordenada_y; salir_del_bucle := true; end else begin salida_escritura(coordenada_x, coordenada_y); salida_escritura(candidata_x, candidata_y); end end; // if tablero[...] = vacia else salida_lectura(candidata_x, candidata_y); end // if (importa_visitadas...) estoy_mirando_casilla_adyacente_numero := estoy_mirando_casilla_adyacente_numero + 1; end // while estoy_mirando_casilla_adyacente_numero if importa_visitadas = true then // Hemos terminado la primera vuelta de inspeccion // y seguimos sin casilla para mover importa_visitadas := false; else // Hemos hecho las dos vueltas y no hemos encontrado nada salir_del_bucle := true; end // while not salir_de_bucle; end // while coordenada_x ... wait(mutex_mensaje); writeln (’Soy el coco ’,id,’ como ya he llegado al borde del tablero me voy’); signal(mutex_mensaje); end // process coco 5 Programación Concurrente: Septiembre 2007 David Fernández-Amorós [email protected] var cocos : array[1..Ncocos] of coco; // Programa principal begin for i:=1 to N do for j:=1 to M do tablero[i][j] := vacia; // Codigo para poner las paredes en su sitio ... // Paredes colocadas init(mutex_mensaje, 1); // Ahora elegimos unas coordenadas iniciales para los cocos for i := 1 to Ncocos do begin repeat // Elegimos al azar dos numeros. La coordenada x entre 2 y N-1 y la y entre 2 y M - 1, // no queremos que la posición inicial esté en la zona de salida del tablero coordenada_x[i] = random (2..N-1); coordenada_y[i] = random(2..M-1); until tablero[coordenada_x[i]][coordenada_y[i]] = vacia; tablero[coordenada_x[i]][coordenada_y[i]] = uncoco; end; // Inicializamos las estructuras para los lectores y escritores for i:= 1 to N do for j:= 1 to M do begin init(mutex[i,j],1); init(escritor[i,j], 0); init(lector[i,j], 0); nl[i,j] = 0; nle[i,j] = 0; nee[i,j] = 0; escribiendo[i,j] = false; end cobegin for i := 1 to Ncocos do cocos[i] = coco(i, coordenada_x[i], coordenada_y[j]); coend end. 6