Apuntes de Concurrencia 3
Apuntes de Concurrencia 3
Apuntes de Concurrencia 3
Cada lunes se entregar un problema que debe resolverse antes del jueves. nicamente sirven
para practicar y para salvar a alguien que est casi aprobado. 10 ejercicios.
Parte terica 50% (2 exmenes)
Parte prctica 50% (2 partes) En grupos de 2 personas.
Cada examen debe superarse con al menos un 4 para hacer media.
Tema 1
Ejercicio 0
Leer Concepts and Notations for Concurrent Programming de G.R. Andrews y F.B. Schneider (1983)
hasta la seccin 3.2 (incluida).
Djsktra
Hoare
Knuth
La primera lnea del cdigo (a1=new A();)crea en el heap (en la direccin de memoria
0001, por ejemplo) una abstraccin de un objeto A, con su variable y sus mtodos.
Entonces llega la parte donde se ejecutan los sets. El set inserta en la variable x de cada objeto
lo que se le pase a la funcin. Al imprimir se imprime dicho nmero por pantalla.
Lo interesante en esta parte es el orden en que aparecen las sentencias de asignacin:
a1 = a2
a3 = a2
Por tanto, las direcciones de memoria de a1 y a3 son iguales que la de a2. Por ello, cuando se
hace el set de a3 es el nico que importa, pues los anteriores se escriben en el mismo sitio.
Conceptos
Hilo de ejecucin (Threads o procesos)
En concurrencia trabajaremos con muchos hilos de ejecucin. Un thread o proceso es lo
mismo en con concurrencia. Para poder disponer de varios hilos de ejecucin existen varias
tcnicas, nosotros vamos a utilizar el concepto de Fork.
Fork
Conceptualmente es un punto en el que se crea un nuevo hilo de ejecucin que ejecuta de
forma simultnea.
El fork del mtodo ejemplo ejecuta ese
mtodo en un nuevo thread. Este thread tiene
un entorno de ejecucin diferente, con
variables locales diferentes, etc. Ambos
threads solo comparten una cosa: la memoria.
Las variables globales son iguales para ambos.
Es decir, si tratas de asignar un valor a una
variable global se asignarn a la vez. Es decir,
no sabr qu proceso ejecuta primero ya que
entra en juego la sensibilizacin de la
memoria, solo puede ser accedida una vez al
tiempo, pero no sabes cul va a ejecutarse primero. Ese es uno de los problemas de la
concurrencia.
Sincronizacin
En muchas ocasiones ser necesarios sincronizar la ejecucin de varios threads, esto es algo
que dista de ser trivial.
Semforos
Es un tipo abstracto de datos muy til para sincronizar procesos:
interface Semaphore{
void Wait();// Decrementa el contado si no es 0, si lo es
// mantiene en espera al proceso hasta que deje
// de serlo.
void Signal();// Incrementa el contado
}
Los semforos se encuentran en los sistemas operativos.
Definicin de concurrencia
Concurrencia en Java
Cmo pueden establecerse threads en Java?
Clase Thread
Existe una clase predefinida en Java llamada Thread, con tres operaciones:
start()
join()
run()
EJemplo
void main(){
Thread t = new PrimerThread(); //Crea un objeto de la clase
//PrimerThread
t.start(); //start es el mtodo ms importante. Lanza un
//nuevo proceso en tiempo despreciable y ejecuta
//el mtodo run() del thread al que se llama. (1)
System.out.println(Hola soy el thread principal);
System.out.println(He puesto en marcha un proceso);
t.join();//Este mtodo espera a que el hilo de ejecucin
//llamada (t) termina para continuar su propia
//ejecucin.
}
class PrimerThread extends Thread{
public void run(){//Este run es el main del segundo proceso
System.out.println(Hola soy un PrimerThread y acabo
de arrancar);
sleep(1000);//espera 1 segundo
System.out.println(Hola soy un PrimerThread y voy a
terminar);
}
}
(1) Si en vez de t.start() hubisemos colocado t.run() entonces no se creara un proceso nuevo
sino que continuara de forma secuencial. Es decir, ejecutara run() y, despus, ejecutara la
lnea despus de main. Con start() aseguramos cual se ejecuta primero y donde.
Si en la clase PrimerThread, aadiremos las lneas;
Thread t = new PrimerThread();
t.start();
4
Por tanto, una clase Thread puede crear hilos de ejecucin tambin.
Ejercicio 1
Creacin de threads en java
public class CC_01_Threads{
public static void main(String args[])
{
int N=1;
Thread t[] = new Thread[N]; //Crea un array de thread
for(int i = 0; i < N; i++){
t[i] = new PrimerThread; //Crea los elementos de
//dicho array.
}
for(int i = 0; i < N; i++){
t[i].start(); //Pone en marcha todos los threads
}
System.out.println(Todos los procesos en marcha);
for(int i = 0; i < N; i++){
t[i].join(); //Espera a que terminen todos los
//procesos.
}
System.out.println(Todos los procesos terminados);
}
}
En el run() de la clase thread se escriben cosas y se espera. Por mucho que aumentemos N el
programa no tarda mucho ms que el tiempo de espera de un thread. Eso es concurrencia.
Ejercicio 2
Provocar una condicin de carrera
public class CC_02_carrera
{
public static void main(String [] argv) threads Exception
{
final int M=2;
Thread t[] = new Thread[M];
for(int i = 0; i < M; i++){
t[i] = new Incrementado;
}
for(int i = 0; i < M; i++){
t[i].start();
}
for(int i = 0; i < M; i++){
t[i].join();
}
System.out.println(Incrementador.n);
}
Class Incrementador extends Thread
{
final private static int N = 10;
static int n=0;
public void run()
{
for(int i = 0; i < N; i++){
n++;
}
}
IMPORTANTE
Si no es static cada objeto tendra
su propia n
}
Otra forma de compartir n sera creando una clase para ello:
public class Entero
{
public int = 0;
}
y creando un objeto de esta clase en el main:
Entero compartido = new Entero();
y se construye el constructor en Incrementador:
public Incrementador(Entero compartido)
{
this.compartido = compartido;
}
aumentarlo en cada iteracin:
6
compartido.n++
y, finalmente, al imprimirlo:
System.out.println(compartido.n)
Al ejecutar este programa se observa que no siempre se obtiene lo que se debe ya que existen
iteraciones en las que no se incrementa la variable. Esto sucede porque existen condiciones de
carrera, es decir, porque dos procesos han intentado acceder a la misma variable al mismo
tiempo. Como un acceso a memoria no puede hacerse de forma concurrente, primero lo har
uno proceso y despus otro, haciendo que no seamos capaces de conocer de forma precisa el
resultado final.
Volatile
Esta palabra reservada de Java crea una barrera para la cach que impide (conceptualmente)
que se utilicen dos variables (idnticas) en dos cachs diferentes, tcnica que se utiliza en los
procesadores multicore donde cada core tiene una cach diferente.
Ejercicio 3
Garantizar exclusin mutua con espera activa
class CC_03_MutexEA{
//numero de veces que los procesos repiten su labor
static final int nPasos = 10000;
//variable compartida
volatile static int n = 0;
//seccin no crtica
static void no_sc(){
...
}
static void sc_inc(){
n++;
}
static void sc_dec(){
n--;
}
}
La seccin crtica de cada proceso no puede ser ejecutada al mismo tiempo:
static class Incrementador extends Thread{
public void run(){
for(int i = 0; i < nPasos; i++){
//Seccin no crtica
no_sc();
//Seccin crtica
sc_inc();
7
}
}
}
Qu va a suceder?
En el instante de tiempo 1 (imagen
de la izquierda) el incrementador ha
terminado de ejecutar no_sc() y va a
ejecutar sc_inc(), pero si se lo permitimos durante el tiempo en que
ambas secciones crticas ejecutan a la
vez puede darse una condicin de
carrera. Lo que debemos conseguir
es que un proceso espere a otro. Sin
embargo, debe hacerse sin herramientas de concurrencia (en este
ejercicio), solo mediante control de
flujo y espera activa.
Bsicamente un while:
no_sc();
while(dec no ejecuta sc){
sc_inc();
}
Por tanto el esquema de lo que debe suceder ser algo diferente.
Para llegar a esta solucin una idea plausible sera tomar una variable booleana:
//variable para asegurar mutex
volatile static boolean en_sc = false;
//si nadie est ejecutando est a false
Es muy importante la etiqueta volatile. Ahora el while pasara a tener un formato similar a:
while(en_sc){}//comprueba si alguien est ejecutando la sc
//y espera
en_sc = true;
sc_inc();
en_sc = false;
}
Esto parece una solucin (en el diagrama
de la derecha), pero no lo es. Ya que si los
dos entran al mismo tiempo en el while se
dar una condicin de carrera de todas
formas.
Otra idea es aadir una variable
compartida ms que establezca prioridad
a los procesos. Entonces:
volatile static boolean prioridad_inc = true; //Prioridad para
//el incrementador
INC //esperar si no tengo la prioridad y en_sc
while(!prioridad_inc || en_sc){}
en_sc = true;
sc_inc();
prioridad_inc = false;
en_sc = false;
DEC //esperar si no tengo la prioridad y en_sc
while(prioridad_inc || en_sc){}
en_sc = true;
sc_dec();
prioridad_inc = true;
en_sc = false;
Tendramos dos diagramas diferentes si en la condicin del while establecisemos un AND en
lugar de un OR.
Sin embargo, no tendremos que preocuparnos de ello puesto que no es una solucin vlida.
Otra idea es la de crear dos variables booleanas. Entonces:
volatile static boolean en_sc_inc = false; //cierto cuando inc
//entra en sc
volatile static boolean en_sc_dec = false; //cierto cuando dec
//entra en sc
10
en_sc_inc = true;
while(en_sc_dec){}//si dec est en sc, inc espera
sc_inc();
en_sc_inc = false;
[y de forma simtrica en el dec]
en_sc_dec = true;
while(en_sc_inc){}
sc_dec();
en_sc_dec = false;
Es es una solucin invlida, tambin, ya que si se ejecutan los dos procesos al mismo tiempo,
ninguno entrar en la sc, a esto se le denomina interbloqueo o deadlock.
Una ltima solucin podra ser utilizar una variable compartida que se modifique adems de
las variables independientes. En un mtodo se haga verdad y en el otro falso y sea tambin
argumento para el while. Esto sigue sin ser una solucin.
Solucin al ejercicio 3
En incrementador
en_sc_inc = true;
turno_inc = false;
while(en_sc_dec && !turno_inc){}
sc_inc();
en_sc_inc = false;
En decrementador
en_sc_dec = true;
turno_inc = true;
while(en_sc_inc && turno_inc){}
sc_dec();
en_sc_dec = false;
Propiedades indeseables
Mecanismos de concurrencia
Semforos
Es un TAD con dos operaciones atmicas, esperar y sealizar:
class semforo{
private int cont = 0; //Contados, a 0 en este ejemplo.
public void esperar(){
11
Siempre estn las operaciones esperar y sealizar, hay que mirar como se denominan
en esa librera. En Java: esperar = acquire y sealizar = release.
Hay que mirar cmo se construyen los semforos. En Java: El constructor se llama con
uno o dos parmetros:
12
o
o
Un contador (int) que ser el nmero inicial del contador del semforo.
Parmetro obligatorio.
Un parmetro booleano que construir el semforo con equitatividad fuerte
(si true) o sin equitatividad (a false). Es un parmetro opcional.
Ejercicio 5 y 6
Almacn de uno y varios datos con semforos
Productor
while(true){
p = fabrica.producir();
//sc
almacenCompartido.almacen(p);
//sc
}
Ya hemos aprendido a solucionar la exclusin mutua (mutex), trivial con semforos (ejercicio 4
del fichero de ejercicios, no se especifica en estos apuntes). El problema realmente complicado
ahora es la sincronizacin condicional. Veamos un ejemplo:
Almacn
Class Almacen1 implements Almacen
//Producto a almacenar: null representa que no hay producto
Cuando un consumidor quiere un producto:
//mutex == 0 proceso en sc
//Dato == 1 almacenado != null
public producto extends ...
.
.
.
//sc if (algo == null)
mutex.await();
Diagrama 1
mutex.signal();
no_vacio.await();
mutex.await();
}
almacenado = null;
mutex.signal();
no_lleno.signal();
Este ejemplo es muy difcil de encontrar debido a la dificultad de garantizar la solucin
correcta con semforos. Adems, no es la solucin ms ptima, pues si almacenado = null:
Especificacin de recursos
Resumen de los apuntes oficiales
Idea general: procesos y recursos para comunicarse.
Procesos: cdigo.
Recursos:
o Nombre (como en una clase).
o Operaciones y datos (como en una clase)
o Semntica.
Sobre nuestro ejemplo:
o Semntica
Datos.
Mutex (gratis).
Sincronizacin condicional (CPRE).
Operaciones: PRE-POST (siguen reglas lgicas).
Ejercicio 7
Especificacin de un recurso compartido
16
C-TAD ControlAccesoPuente
Operaciones
ACCION
ACCION
Solicitar_entrada: Entrada[e]
Avisar_salida: Salida[e]
SEMANTICA
DOMINIO
TIPO: ControlAccesoPuente = (sn: N x ns: N)
TIPO: Un controlador para los coches que van surnorte y otro para los que van norte-sur. Uno de ellos
siempre debe ser 0.
Donde: Entrada = EN/ES
Salida = SN/SS
INVARIANTE: self.sn > 0 self.ns
self.ns > 0 self.sn
CPRE: Si se entra por el sur no puede haber coches nortesur, si se entra por el norte no puede haber coches surnorte.
PRE: e = ES self.ns = 0 V e = EN self.sn = 0
Solicitar_entrada(e)
POST: e = ES self.sn = self.snPRE +1 self.ns = self.nsPRE
V e = EN self.ns = self nsPRE + 1 self.sn = self.snPRE
CPRE: Es imposible que esta operacin sea ejecutada ms de una vez al tiempo por
culpa de un coche que no est cruzando el puente, por lo que siempre puede ser
ejecutada.
CPRE: Cierto
avisar_salida(s)
POST: Decrementamos el nmero de coches en la direccin adecuada.
POST: S = SS self.sn = self.snPRE self.ns = self.nsPRE -1
V S=SN self.ns = self.ns PRE self.sn = self.snPRE -1
18