Concurrencia

Anuncio
Concurrencia en Java
Concurrencia y Distribución
Programación Avanzada
Posgrado en Ciencia e Ingenierı́a de la Computación, UNAM
1.
El mecanismo de threads
Una aplicación Java o applet puede contener secciones de código que se
ejecutan simultáneamente. Una sección de código que se ejecuta independientemente se conoce con el nombre de thread ó proceso ligero. Un thread
comparte el espacio de direcciones con la aplicación principal o applet, y por
tanto, puede accesar a los datos que son visibles a la aplicación principal o
applet, ası́ como a cualquier otro thread que se ejecute concurrentemente.
De tal modo, debe tenerse mucho cuidado al accesar los datos compartidos,
ya que un thread puede estar modificando un dato compartido mientras otro
lo está leyendo.
Los detalles exactos de cómo se implementa un thread dependen de la
máquina donde la aplicación o applet se ejecuta. En una máquina multiprocesador puede haber una verdadera ejecución simultánea de threads,
mientras que en una máquina uniprocesador la ejecución simultánea se simula mediante conmutar rápidamente entre threads individuales para dar la
ilusión de una ejecución simultánea.
Por ejemplo, considérese una aplicación en Java que prueba si un número
natural es primo, y puede realizar esta actividad mientras que otros threads
realizan otras actividades. El método estático isPrime() en la clase Useful
prueba si un número long es primo o no. Sin embargo, este proceso toma
bastante tiempo si el número es grande. De tal modo, el código de la clase
Useful se muestra a continuación:
class Useful {
public static boolean isPrime(long n){
if (n <= 0 || n%2 == 0) return false;
long rootN = (long) (Math.sqrt((double) n)) + 1;
1
for (long i == 3; i <= rootN; i+=2){
if (n%i == 0) return false;
}
return true;
}
}
Para crear un “objeto activo”, se requiere que tal objeto implemente la
interfaz Runnable. Esta interfaz define un solo método llamado run() que se
sobrecarga con el método que eventualmente ejecuta un thread por separado.
La interfaz Runnable se define como sigue:
interface Runnable{
public abstract void run();
}
La clase Prime implementa la interfaz Runnable:
class Prime implements Runnable{
private long theNumber;
private boolean theResult;
...
}
El constructor de la clase registra cualquier dato de entrada que requiera
el thread. En este caso en particular, el número que se comprueba si es primo:
public Prime(final long n){
theNumber = n;
}
El método run() implementa el cómputo para determinar si el número
es primo. El cuerpo de este método llama al método estático isPrime()
para realizar tal cómputo.
public void run(){
theResult = Useful.isPrime(theNumber);
}
El método result() se usa para retornar el resultado del cómputo que
se ha almacenado en la variable theResult de tipo boolean.
2
public boolean result(){
return theResult;
}
Nótese que el método result presenta la respuesta correcta sólo si el método
run() ha terminado.
1.1.
Poniendo todo junto
Una instancia de la clase Prime se crea inicialmente con la responsabilidad de implementar las acciones que realice el thread.
Prime prime = new Prime(99);
Nótese que esta clase implementa la interfaz Runnable.
El objeto prime se “envuelve” por una instancia de la clase Thread para
crear un objecto activo thread.
Thread thread = new Thread(prime);
El objeto activo thread se inicia mediante enviarle el mensaje start().
thread.start();
Esto prepara un thread nuevo ejecutándose separadamente, que realiza
el método run() en la clase Prime. Este proceso se muestra en la Figura 1,
que muestra el comienzo de la ejecución del thread recién creado.
Para obtener el resultado correcto del objeto prime, el cómputo debe
haber finalizado. Una llamada al método join() de la clase Thread causa
que temporalmente se espere hasta que el thread ha terminado de ejecutarse. Por ejemplo, el siguiente código causa una espera hasta que el thread
independiente que se está ejecutando haya terminado.
thread.join();
La Figura 2 muestra al programa principal esperando por el objeto activo thread después de llamar al método join(). El programa principal se
suspende hasta que la unión se lleva a cabo. Naturalmente, si el objeto activo
ha terminado de ejecutarse, el programa principal continúa inmediatamente.
3
Threads
2
thread
1
programa principal
thread.start()
Tiempo
Figura 1: Dos threads ejecutándose.
Threads
2
thread
1
programa principal
thread.join()
thread.start()
Tiempo
Figura 2: El programa principal espera hasta que el thread termina.
4
1.2.
El programa completo
El programa completo para crear separadamente un thread para verificar
si el número 99 es primo se muestra a continuación. Mientras que el cómputo
se lleva a cabo, el programa principal puede simultáneamente realizar otro
tipo de acciones.
class Main {
public statuc void main(String args[]) {
try {
long number = 99;
Prime prime = new Prime(number);
Thread thread = new Thread(prime);
thread.start();
// Otras acciones
thread.join();
System.out.println("El numero " + number +
(prime.result() ? " es " : " no es " ) + "primo");
}
catch (InterruptedException exc) { }
}
}
Nótese la necesidad de proveer un manejador de exceptiones InterruptedException.
En este caso, tal manejador es ignorado.
2.
La clase java.lang.Thread
Los principales métodos de la clase Thread son:
5
Método
Thread()
Thread(name)
Thread(fo)
Thread(fo,name)
activeCount()
destroy()
getName()
isAlive()
join()
join(delay)
run()
sleep(delay)
start()
yield()
Responsabilidad
Crea un nuevo thread.
Crea un nuevo thread con el nombre name.
Crea un nuevo thread que ejecuta el método
run de la clase fo (ésta implementa la interfaz
Runnable).
Crea un nuevo thread con el nombre name
que ejecuta el método run de la clase fo (ésta
implementa la interfaz Runnable).
Retorna el número de threads activos en
el grupo de threads actual.
Destruye el thread. No hay recolección de basura.
Retorna el nombre del thread.
Retorna true si el thread está activo.
Espera a que el thread termine.
Espera al menos un tiempo delay en
milisegundos para que el thread termine.
Ejecuta el objecto función del thread que
implementa la interfaz Runnable.
Causa que el thread “duerma” por
delay milisegundos.
Causa el inicio de actividad del thread,
llamando al método run() de la clase.
Después de detener temporalmente al thread,
permite a otros threads continuar.
Los métodos de la clase java.lang.Object que interactúan con los threads son:
6
Método
notifyAll()
notify()
wait(delay)
wait()
3.
Responsabilidad
Despierta a todos los threads que esperan
a este objeto. Un thread entra en estado de espera cuando
invoca alguno de los métodos wait.
Despierta a un solo thread que espera a este
objeto.
Espera hasta que las siguientes dos condiciones ocurran:
(a) Un método notify() ó notifyAll() se llama desde
otro thread de este objeto;
(b) El tiempo delay en milisegundos ha transcurrido.
Espera hasta que uno de los métodos notify()
ó notifyAll() se llama desde otro thread de este objeto.
Heredando de la clase Thread
Una forma alternativa de implementar un thread en Java es heredar
directamente de la clase Thread. Por ejemplo, el programa anterior podrı́a
haberse escrito como sigue:
class Prime extends Thread{
private long theNumber;
private boolean theAnswer;
public Prime(final long number){
theNumber = number;
}
public void run(){
theAnswer = Useful.isPrime(theNumber);
}
public boolean result(){
return theAnswer;
}
}
Entonces, después de que la instancia de la clase Prime se crea, su método
run() se invoca mediante el método start(). Un nuevo programa completo
se muestra a continuación que utiliza la nueva clase Prime para implementar
un thread por separado.
class Main {
7
public statuc void main(String args[]) {
try {
long number = 99;
Prime thread = new Prime(number);
thread.start();
// Otras acciones
thread.join();
System.out.println("El numero " + number +
(prime.result() ? " es " : " no es " ) + "primo");
}
catch (InterruptedException exc) { }
}
}
4.
Exclusión mutua y secciones crı́ticas
En muchos casos de ejecución real, las secciones de código no deben ejecutarse concurrentemente. El ejemplo clásico es añadir o remover datos en
un buffer compartido. Por ejemplo, para realizar una copia entre dos dispositivos separados, se puede utilizar un buffer compartido para emparejar
las diferencias en tiempo de respuesta. Esto se ilustra en el diagrama de la
Figura 3.
read
Disco
Reader
thread
Writer
thread
put
write
Disco
get
Datos compartidos
Figura 3: Se copia mediante un buffer para emparejar las diferencias en
velocidades de lectura y escritura.
El problema es prevenir que ambos threads de lectura y escritura accese
simultáneamente al buffer, causando la consecuente corrupción de ı́ndices y
datos.
Para ello, Java permite la creación de un monitor. En esencia, un mo8
nitor es un objeto con métodos especiales sincronizados. Sólo uno de los
métodos sincronizados puede ejecutarse en un momento dado de tiempo. Si
otro thread intenta acceder a un método sincronizado mientras otro método
sincronizado está siendo ejecutado, la solicitud se encola hasta que el método
sincronizado que actualmente se ejecuta termine.
En Java, un monitor se implementa como una clase cuyos métodos se
declaran como synchronized. Cuando un mensaje se envı́a a un método
que ha sido declarado como synchronized, el método se ejecuta sólamente
si no hay un “candado” sobre el objeto. Si el objeto tiene candado, el proceso
que envió el mensaje se detiene temporalmente hasta que el objeto deja de
tener candado. Un objeto tiene candado cuando un método synchronized
se invoca, y deja de tenerlo cuando el método termina.
Un método puede causar que un objeto deje de tener candado mediante
ejecutar el método wait(). Sin embargo, esto causa que el objeto se suspenda
hasta que otro thread envie el mensaje notify() ó notifyAll() al objeto.
Un thread reactivado que se detuvo al ejecutar el método wait() pondrá de
nuevo el candao al objeto. Obviamente, si hay dos o más threads esperando
con el método wait(), sólo uno de ellos puede ser reactivado.
5.
La implementación
Desarrolle un programa concurrente Copy que implemente la copia eficiente de disco a disco, emparejando las diferencias de velocidad, mediante
dos threads y un monitor, de un archivo de texto “origen” a otro archivo de
texto “destino”. El monitor se utliza para proveer un acceso serializado al
buffer de datos compartidos. Los datos pueden añadirse al buffer y removerse del buffer, pero estas operaciones no pueden realizarse simultáneamente.
Esto se muestra en la Figura 4.
Las responsabilidades de los componentes individuales se muestran a
continuación:
Clase
Reader
Writer
Buffer
Instancia
thread
(reader)
thread
(writer)
monitor
(buffer)
Responsabilidades
Leer datos del archivo de entrada y escribirlos en
el buffer. Debe bloquearse si el buffer se llena.
Tomar datos del buffer y escribirlos en el archivo
de salida. Debe bloquearse si el buffer está vacı́o.
Serializar el almacenamiento y recuperación de
los datos a y desde el buffer.
9
read
Disco
Reader
thread
Writer
thread
put
write
Disco
get
Datos compartidos
Figura 4: Copy se implementa utilizando dos threads y un objeto buffer
protegido.
5.1.
La clase Buffer
La clase Buffer debe contener dos métodos sincronizados put() y get()
que permitan la entrada y salida de datos al buffer respectivamente. El buffer
se implementa como una cola. El primer elemento de entrada al buffer será el
primer elemento que se saque del buffer. La implementación de la cola debe
utilizar un arreglo para simular las propiedades de una cola. Para hacer
este proceso lo más general posible, la cola debe implementarse como una
colección de elementos de tipo Object, de modo que cualquier objeto pueda
colocarse en la cola. La Figura 5 muestra una cola con cuatro objetos a, b,
c y d que han sido encolados en la cola. Los objetos se incorporan al final
de la cola, y se remueven de la cabeza.
theHead
theTail
a
b
c
d
Figura 5: Una cola implementada con un arreglo.
La cola debe implementarse como un arreglo con apuntadores int para
representar la cabeza (theHead) y la cola (theTail). Se debe incluir también
10
un contador theNoOfObjects que lleva la cuenta de los elementos en la
cola. Este contador debe utilizarse para eliminar la ambigüedad entre la
cola “llena” y la cola “vacı́a”.
5.2.
La clase Reader
La clase Reader debe ser una subclase de Thread. El constructor debe
registrar el archivo desde donde se leen los datos, ası́ como el objeto buffer
que almacena temporalmente los datos. Su método run() debe ser capaz de
abrir el archivo de entrada, convertir cada lı́nea del archivo en una cadena
(string) y añadirla al buffer. También la clase debe considerar y manejar
cualquier excepción con sus respectivos bloques catch. Cuando ocurre un
error o se alcanza el final del archivo, un objeto null debe añadirse al buffer,
lo que signica que ya no hay más objetos a ser añadidos al buffer.
5.3.
La clase Writer
La clase Writer debe ser también una subclase de Thread. La instancia
de la clase Writer debe ser capaza de recabar instancias de tipo string
del buffer, y escribirlas en el archivo de salida. Ası́, su constructor debe
registrar el archivo a donde se escriben los datos, ası́ como el objeto buffer. La
implementación del método run() debe abrir el archivo de salida y escribir
los datos recabados del buffer ahı́. Un buffer vacı́o se representa mediante
retornar el valor null para el objeto recabado. Todo error de entrada/salida
debe ser capturado, reportando su naturaleza al usuario del programa.
5.4.
La clase Copy
La clase principal Copy crea todos los objetos necesarios con los parámetros de archivos de entrada y salida. Una vez iniciado, los threads deben
ejecutarse independientemente, añadiendo y sacando datos del buffer compartido hasta que se terminen los datos. Una vez finalizada la operación, el
programa termina.
Una vez compilado, el programa debe realizar una operación de copia.
Por ejemplo, supóngase que se copian los contenidos de un archivo from.dat
a un archivo to.dat. Esto debe lograrse mediante la instrucción en lı́nea:
java Copy from.dat to.dat
11
Referencias
[1] Ken Arnold and James Gosling. The Java Programming Language.
Addison-Wesley, 1996.
[2] Barry Boone. Java Essentials for C and C++ Programmers. AddisonWesley, 1996.
[3] Brinch Hansen, P. The Programming Language Concurrent Pascal. In
IEEE Transactions on Software Engineering, 1(2), 1975, pp. 199-207.
[4] Gary Cornell and Cay S. Horstsmann. Core Java. Prentice-Hall, 1996.
[5] David Flanagan. Java in a Nutshell. O’Reilly, 1996.
[6] Hoare, C.A.R. Monitors : An Operating System Structuring Concept.
In Communications of the ACM 17, 1974, pp.549-557.
12
Descargar