Download Tras un tiempo sin Curso de Java, volvemos hoy con un tema muy

Document related concepts
no text concepts found
Transcript
Tras un tiempo sin Curso de Java, volvemos hoy con un tema muy importante para
conseguir el máximo rendimiento de nuestro programa utilizando todos los núcleos de
nuestro procesador.
La programación concurrente es capaz de realizar varias tareas de forma simultánea.
Hay varios tipos de concurrencia, nosotros trabajaremos con programación de
memoria común, donde tan sólo tendremos una memoria RAM que es compartida por
los demás programas y tareas.
Hay un aspecto muy importante para lograr que la concurrencia sea correcta:

El resultado debe ser el mismo si se hace con un procesador que con cuatro
procesadores. Es decir, el resultado no debe depender del número de
núcleos/procesadores del ordenador.
Nota: Hablaremos de proceso = hilo.
Los sistemas operativos actuales permiten la concurrencia de procesos, el propio
sistema operativo se encarga de permitir el uso de variables compartidas para pasar
datos de un proceso a otro y controlar las regiones críticas.
¿Qué es una región crítica?
Es un trozo de código, en el que la correción del programa se ve comprometido por el
uso de variables compartidas. Un proceso sólo podrá acceder a esta región crítica
durante un tiempo determinado para que no halla inanición. Java permite de forma
intrínseca la concurrencia.
Para garantizar la corrección del programa, en Java tenemos varios métodos que
podemos utilizar:

Monitores: Un monitor implementa una región crítica condicional, de tal forma
que podemos sacar de la cola de espera a uno o a todos los procesos esperando.
Para que sea un monitor, todos los métodos de la clase deben ser synchronized.
Para el uso de monitores, nos ayudamos de los métodos:
1. wait(): Si no se cumple la condición, esperamos. notify(): Cuando hemos
entrado en la región crítica, y hemos hecho cierta acción, notificamos a un
proceso que hay esperando para entrar si se cumple la condición (le despertamos
del wait()).
2.
notifyAll(): Igual que el anterior pero notificamos a todos los hilos que hay
esperando.

Semáforos: El nombre de semáforos es como en la vida real, un semáforo
cerrado no podrán pasar coches hacia un lado, y un semáforo abierto sí podrán.
Los semáforos garantizan la exclusión mutua y la sincronización (para que los
coches no se choquen en la región crítica que en este caso es el cruce). En
semáforos nos ayudamos de varios métodos como por ejemplo:
1. acquire(): Para adquirir el semáforo (ponerlo en verde para la cola de coches A)
una vez que lo hemos adquirido pueden pasar los coches porque está en verde.
2. release(): El último coche en pasar hace un realease() para que los coches que
están esperando del otro semáforo puedan pasar ya que nosotros hemos
terminado.
Los semáforos se usan para controlar el número de hilos que pueden acceder a un
recurso. Un proceso bloqueado en el semáforo, puede ser liberado por otro, esto no
ocurre en los locks que veremos a continuación.

Locks: Los locks proporciona mayor rendimiento, con la misma semántica que
la sincronización. Soporta timeout al adquirir un bloqueo e incluso soporte para
interrumpir un hilo. Podemos decir que con los locks controlamos más lo que
hace nuestro programa, es más "manual", y por tanto, se necesita la experiencia
del programador para que no tengamos fallo en el programa.
Todo esto es básicamente lo que veremos en concurrencia de memoria común,
ampliaremos algo de teoría, pero sobre todo pondremos ejemplos para que veáis como
funciona.
¿Concurrencia? ¿Realmente hay diferencia de rendimiento en los programas?
Vamos a calcular un determinado de números primos, de forma secuencial y de forma
concurrente (sin usar regiones críticas).
Clase Primos.java (se encargará de calcular dichos números primos).
[java]public class Primos {
private int x, y, n = 0;
public Primos(int x, int y) {
this.x = x;
this.y = y;
}
private boolean esPrimo(int n) {
int raiz = (int) Math.sqrt((double) n);
for (int i = 2; i <= raiz; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
public void calcular() {
for (int i = x; i <= y; i++) {
if (esPrimo(i)) {
n++;
}
}
}
public int cuantos() {
return n;
}
}[/java]
Programa principal ejecutable de Primos sin concurrencia:
[java]public class CuantosPrimos {
public static void main(String[] args) {
long t0 = (new Date()).getTime();
Primos p1 = new Primos(1, 2000000);
Primos p2 = new Primos(2000001, 4000000);
Primos p3 = new Primos(4000001, 6000000);
Primos p4 = new Primos(6000001, 8000000);
Primos p5 = new Primos(8000001, 10000000);
p1.calcular();
p2.calcular();
p3.calcular();
p4.calcular();
p5.calcular();
int n = p1.cuantos() + p2.cuantos() + p3.cuantos() + p4.cuantos() +
p5.cuantos();
long t1 = (new Date()).getTime();
System.out.println("Número de primos menores que 10000000: " + n + "
calculado en " + (t1 - t0) + " miliseg.");
}
}[/java]
Número de primos menores que 10000000: 664580 calculado en 7491 miliseg.
Ahora os voy a poner la clase de Primos que se hace de forma concurrente. Para la
concurrencia podemos extender la clase Thread, o implementar la interfaz Runnable.
¿Cuando debemos usar una u otra? Debido a que al extender la clase Thread,
estamos heredando todos sus métodos, si queremos que una clase herede de otra, y
encima que sea concurrente, no podremos hereder (extends) de ambas clases, ya que
Java no permite la herencia múltiple. De esta forma hacemos un extends Padre
implements Runnable, para heredar de Padre e implementar la concurrencia. Por tanto,
implementando la interfaz tenemos más "flexibilidad" a la hora de programar nuestras
clases porque esa interfaz la podemos modificar a nuestro antojo para agregarle más
funcionalidades.
Interfaz Runnable:
[java]public interface Runnable {
public abstract void run() ;
}[/java]
Extendiendo la clase Thread quedaría:
[java]public class PrimosThread extends Thread {
private int x, y, n = 0;
public PrimosThread(int x, int y) {
this.x = x;
this.y = y;
}
private boolean esPrimo(int n) {
int raiz = (int) Math.sqrt((double) n);
for (int i = 2; i <= raiz; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
@Override
public void run() {
for (int i = x; i <= y; i++) {
if (esPrimo(i)) {
n++;
}
}
}
public int cuantos() {
return n;
}
}[/java]
Implementando la clase Runnable, sustituímos: extends Thread por implements
Runnable y listo, no tendremos que cambiar nada más en esta clase.
Programa Principal de Primos con extends Thread:
[java]public class CuantosPrimos2 {
public static void main(String[] args) {
long t0 = (new Date()).getTime();
PrimosThread p1 = new PrimosThread(1, 2000000);
PrimosThread p2 = new PrimosThread(2000001, 4000000);
PrimosThread p3 = new PrimosThread(4000001, 6000000);
PrimosThread p4 = new PrimosThread(6000001, 8000000);
PrimosThread p5 = new PrimosThread(8000001, 10000000);
p1.start();
p2.start();
p3.start();
p4.start();
p5.start();
try {
p1.join();
p2.join();
p3.join();
p4.join();
p5.join();
} catch (InterruptedException e) {
}
int n = p1.cuantos() + p2.cuantos() + p3.cuantos() + p4.cuantos() +
p5.cuantos();
long t1 = (new Date()).getTime();
System.out.println("Número de primos menores que 10000000: " + n + "
calculado en " + (t1 - t0) + " miliseg.");
}
}[/java]
Programa principal con implements Runnable:
[java]public class CuantosPrimos2 {
public static void main(String[] args) {
long t0 = (new Date()).getTime();
PrimosThread p1 = new PrimosThread(1, 2000000);
PrimosThread p2 = new PrimosThread(2000001, 4000000);
PrimosThread p3 = new PrimosThread(4000001, 6000000);
PrimosThread p4 = new PrimosThread(6000001, 8000000);
PrimosThread p5 = new PrimosThread(8000001, 10000000);
p1.run();
p2.run();
p3.run();
p4.run();
p5.run();
long t5 = (new Date()).getTime();
int n = p1.cuantos() + p2.cuantos() + p3.cuantos() + p4.cuantos() +
p5.cuantos();
System.out.println("Número de primos menores que 10000000: " + n + "
calculado en " + (t5 - t0) + " miliseg.");
}
}[/java]
La diferencia es bastante significativa:
Número de primos menores que 10000000: 664580 calculado en 4262 miliseg.
Calculamos el mismo número de primos en casi la mitad de tiempo. Pruebas realizadas
con un Intel Core2Duo T8300 con 2 núcleos.
Parece que es útil, ¿verdad?
Related documents