Archivos de la categoría ‘11. Concurrencia en Java’

Materiales para la Unidad 11

Publicado: Lunes, 21 \21\UTC junio \21\UTC 2010 en 11. Concurrencia en Java
Presentación

Transcripción de la parte teórica de la unidad 11

Teoría

Ahora ya tenéis la base de la programación orientada a objetos. A partir de este nivel, simplemente debéis especializaros en lo que más os guste y dedicar tiempo y esfuerzo. La diferencia con respecto a otros lenguajes es que, con Java, ese tiempo/esfuerzo se reduce drásticamente.

Y para muestra, en los dos temas que quedan del módulo, veréis un par de ejemplos de lo sencillo que es abordar un problema, por específico que sea. Esto se debe a que, habitualmente, ya hay otras personas (cinco, quinientas, dos mil, un millón) que tienen esos mismos intereses y escriben código para solucionar ese problema o similares.

En el tema 11 veremos la gestión de procesos, en concreto, la concurrencia y sincronización entre procesos. Para que os hagáis una idea, un tema con este contenido utilizando el lenguaje C puede abordarse en aproximadamente un trimestre; sin embargo, nosotros emplearemos una quincena, es decir, seis veces menos tiempo en dar los mismos contenidos. Y además del ahorro de tiempo (que es importante), también hay una importante reducción del esfuerzo (que es, quizás, más importante). La complejidad de implementar la concurrencia en C es abismal, sin embargo, con Java es mucho más natural y se nos da gran cantidad de trabajo ya hecho.

Para el estudio de esta unidad, debéis leer el apartado 5.9 del material que venimos utilizando habitualmente y también el punto 6 de un nuevo documento que os he colgado en el Aula Virtual. Se trata del libro “Aprenda Java como si estuviera en primero” de Javier García de Jalón y otros autores, profesores de la Escuela Superior de Ingenieros de Navarra.

A continuación os haré una breve reseña sobre este tema.

Como ya sabéis, un ordenador tiene un único procesador por lo que únicamente pueden ejecutar una intrucción en un instante de tiempo. A medida que han avanzado los programas y, en concreto, los sistemas operativos, se ha visto la necesidad de poder ejecutar en paralelo varios programas, pero esto es incompatible con lo que hemos dicho anteriormente (un instante/una instrucción).

Los programadores comenzaron a estudiar técnicas para simular este efecto de multiejecución en un instante de tiempo y se llego a lo que hoy conocemos como sistemas operativos multitarea (la mayoría de los actuales). Obviamente es una emulación, por lo tanto consume recursos de nuestro sistema.

Así, actualmente no nos sorprendemos si estamos con nuestro ordenador escribiendo en este maravilloso blog 😉 mientras escuchamos el último éxito de nuestro grupo favorito. En este caso hay dos programas que parecen estar trabajando simultáneamente, el explorador web y el reproductor mp3. Pero esto no es posible, ya que únicamente tenemos un procesador, por lo que en realidad, se el sistema operativo está simulando esta multitarea.

Ahora ya tenemos procesadores de varios núcleos, pero siguen sin resolvernos el problema que abordamos en este tema. Imaginad el caso que tenemos un procesador de cuatro núcleos y necesitamos ejecutar cinco programas. Habrá un momento que siempre necesitaríamos más, por lo que la solución software es lo que necesitamos.

Pero además de esta ejecución de varias tareas (multitarea) también se ha creado la necesidad de ejecutar en un mismo programa varias tareas. Esto se consigue mediante el uso de hilos (threads en inglés). Es decir, además de poder escuchar nuestra música en el reproductor, también necesito (¡al mismo tiempo!) convertir esa música a otro formato para poder grabarme un cd de audio.

¡Y es que siempre pedimos más! Ahora bien, no nos quejemos de qué cosas más complicadas estamos estudiando. Esto nos lo pide el mercado (y vosotros mismos, apuesto a que sí).

Tanto la multitarea como el multihilo conllevan una serie de problemas, que vamos a ver muy por encima en este tema, como por ejemplo el problema del acceso a un trozo de código por parte de varios procesos (sección crítica). Pero esto lo veremos en una entrada del blog.

Hasta entonces, espero que disfrutéis de este tema.

Laboratorio

El laboratorio se visualiza mejor en alta definición (HD) y a pantalla completa.

También tienes la posibilidad de ver el video con subtítulos. Para ello, haz clic sobre el botón “Turn on Captions” (CC).

Undécima Práctica de Evaluación (PEV11)
Práctica de evaluación

La Undécima Práctica de Evaluación del módulo es una práctica dividida en dos partes.

  1. La primera parte trata la creación de un programa multihilo.

  2. La segunda parte trata la sincronización de hilos.

Tenéis una explicación exhaustiva en el enunciado de la prueba.

Recordad que para acceder a la evaluación cuatrimestral, la entrega de todas las PEVs es obligatoria (y cada una de ellas debe tener una nota superior a 2).

Sinopsis de la película "Ciberguerrilla"Documental

Mira el documental “Ciberguerrilla”. A continuación, comenta tus impresiones sobre lo que en él se dice en el foro del Aula Virtual.

Enlace al documental en Megavideo: http://www.megavideo.com/?v=J59XTFCT

Orientaciones didácticas

Las orientaciones didácticas de una unidad son un resumen (no más de un folio) con unas pequeñas pautas  que permiten al alumno hacerse una idea general de qué va a aprender en esa unidad – respecto a los contenidos teóricos – y cómo debe desarrollar la parte práctica.

En estas orientaciones también se incluye un registro organizado con los comentarios realizados por el alumnado durante el desarrollo de la unidad, que seguramente ayudarán a facilitar la comprensión de los conceptos teórico-prácticos.

Aquí tenéis las orientaciones didácticas de la unidad 11:

Anuncios

Concurrencia en Java

Publicado: Sábado, 13 \13\UTC junio \13\UTC 2009 en 11. Concurrencia en Java

Con un único procesador, solamente puede ejecutarse una instrucción en un instante de tiempo.

La frase anterior parece una perogrullada, pero no lo es. Posiblemente estáis frente al ordenador leyendo este texto desde vuestro navegador web al mismo tiempo que escucháis el último disco de vuestro artista favorito. El ordenador está ejecutando dos programas (navegador web y reproductor de música) simultáneamente, cuando en realidad sólo puede ejecutar una instrucción – según hemos afirmado anteriormente.

Esto sucede porque el ordenador tiene tal potencia de proceso que nos da la sensación de estar realizando ambas tareas al mismo tiempo, pero simplemente se trata de una ilusión: el ordenador va intercalando la ejecución con la velocidad adecuada para ofrecernos una percepción de simultaneidad. A la capacidad de ejecutar múltiples programas a la vez se le llama multitarea (también podemos hablar de multiproceso).

Programas y procesos

A medida que avanza la tecnología, los usuarios somos más exigentes, por lo que pedimos mayor capacidad al sistema operativo para que nos permita ejecutar múltiples programas a la vez, con la mayor velocidad posible y que sea lo más cómodo para nosotros. Claro está, todo esto va complicando la programación de estos sistemas operativos y, actualmente, ya es muy difícil encontrar sistemas que no incluyan la multitarea dentro de sus características.

Veamos ahora algo de teoría sobre los procesos.

Un proceso es un programa (p.e. el archivo .class de alguno de vuestros códigos que habéis programado en Java) en ejecución. Es decir, un proceso es ese archivo .class un instante después de haber pulsado el botón Ejecutar de Eclipse. La diferencia entre programa y proceso es que el programa es algo estático (por tanto, no requiere espacio de memoria o tiempo de CPU) y el proceso es algo dinámico (se está ejecutando y requiere tiempo de CPU y almacenar su código en memoria).

Con esta situación, en sistemas operativos que nos ofrecen multitarea/multiproceso podemos ejecutar varios programas (recordad: “ejecutar varios programas” quiere decir “tener varios procesos”) a la vez.

Con esto finalizamos los conceptos teóricos. Ha sido rápido, ¿verdad? Continuemos con ejemplos prácticos.

Aunque los sistemas operativos ya gestionen correctamente la multitarea, los usuarios siguen pidiendo mayor complejidad en las aplicaciones, así que los programadores simplemente pueden echarse las manos a la cabeza y rogar que los usuarios paren de demandar mayores prestaciones o bien ponerse a trabajar en mejorar la funcionalidad de los sistemas para llegar a encontrar soluciones a las peticiones de los usuarios. Obviamente, esta última opción es la que nos interesa y es la que nos llevará a la teoría sobre los hilos. Pero antes, otro ejemplo para entender el problema.

Seguramente, alguna vez os habrá pasado que mientras estáis escuchando con el ordenador en vuestro reproductor de música un disco en formato mp3 (mejor ogg ;-)) os planteáis escucharlo en el coche. Pero el reproductor de CD reproduce únicamente CDs de audio, por lo que tenéis que convertir el disco completo a ese formato. Para ello, posiblemente el propio reproductor de música tiene esa función de conversión.

Fijaos que si podéis escuchar la música y al mismo tiempo convertir el formato, no estáis ejecutando dos procesos (ya que sólo tenéis un reproductor ejecutándose), pero sí que estáis ejecutando dos tareas a la vez.

Hilos

Uppss! ¿Esto no contradice el concepto de proceso? Habíamos dicho que un proceso=una tarea. Por tanto para tener varias tareas necesitamos ejecutar varios procesos. Pues sí y no. Esta “nueva multitarea” se debe a un nuevo concepto del que hablaremos a continuación, llamado hilo. En realidad lo que hacemos es rizar el rizo, es decir, dentro de un mismo proceso repartiremos el trabajo varios subprocesos, para que el usuario tenga la sensación (de nuevo) de estar ejecutando varias tareas a la vez dentro de un mismo programa. A esos subprocesos les llamaremos hilos.

Veamos ahora algo de teoría sobre los hilos.

Un hilo es una parte de un proceso, que tiene variables locales propias y comparte la memoria con el resto de hilos del mismo proceso.

Las programación de aplicaciones que utilizan hilos no suele ser trivial, ya que se debe controlar que el trabajo de un hilo no interfiera con el trabajo de otro hilo en un mismo proceso. Además, en ocasiones, también es necesario que los hilos se coordinen entre ellos. Una introducción a toda esta problemática y cómo se se soluciona es lo que veremos a continuación. Pero antes, seguro que te ha surgido esta pregunta…

¿Por qué utilizar multihilo en lugar de multiproceso?

Dicho de otra manera: si puedo ejecutar varias tareas utilizando varios procesos y esto es más sencillo de programar que utilizar varios hilos de un mismo proceso, ¿por qué utilizar la opción complicada? La respuesta es la rapidez., aunque no voy a extenderme en este tema. Os remito a un buen artículo de la Wikipedia sobre esto.

Creación de hilos

Vayamos al grano. Echad un vistazo a este programa:

_________________

public class UnHilo extends Thread {

public UnHilo(String nombreHilo) {

super(nombreHilo);

}

public void run() {

System.out.println(getName());

}

}

_________________

Código de UnHilo.java

_________________

public class TestUnHilo () {

public static void main (String[] args) {

UnHilo hiloUno = new UnHilo(“HiloUno”);

hiloUno.start();

}

}

_________________

Código de TestUnHilo.java

Tenemos dos clases: UnHilo y TestUnHilo. La primera contiene el código que ejecutará un hilo y la segunda crea una instancia de la primera clase (UnHilo) y la ejecuta.

Veamos cómo se hace todo esto.

Para crear un hilo existen dos formas: implementando la interfaz Runnable de Java o heredando de la clase Thread de Java. Nosotros emplearemos esta última, ya que no hemos estudiado qué es una interfaz, pero sí que sabemos qué es la herencia y cómo trabajar con ella.

Observad en el ejemplo la creación de la clase UnHilo que hereda de la clase Thread. La clase Thread de Java tiene un conjunto de métodos que nos facilitan enormemente el trabajo con hilos. En esta clase, hemos creado:

  • Un constructor con un parámetro de entrada de tipo String (que contendrá el nombre del hilo). En este constructor, simplemente llamamos al constructor de la clase base, enviando el parámetro de entrada con el nombre. Podéis ver la gran cantidad de constructores (un total de 8 que tiene la clase Thread consultando su API.
  • Un método run() que contiene el código con el trabajo que debe realizar el thread. Este es el método más importante. En nuestro caso, es muy sencillo, ya que únicamente imprime por pantalla el nombre del hilo.

Para que el hilo se ejecute, hemos incluido una clase TestUnHilo que contiene el método main(). En esta clase, podéis observar que creamos una instancia del objeto UnHilo y llamamos a su método start(). El método start() no está definido en nuestra clase, sino en la clase base Thread. Al utilizar este método, hacemos que el objeto desde el que se la llama (en nuestro caso, UnHilo) se ponga en ejecución.

El método start() hace una serie de procesos (que son transparentes para nosotros) e invoca al método run() del hilo.

Con esto ya sabemos implementar la creación de hilos mediante la clase Thread.

Terminación de un hilo

Un hilo finalizará cuando termine la ejecución de su método run(). Una vez finaliza, ya no se puede volver a ejecutar. Al estado que se encuentra cuando finaliza se le llama muerto (dead). A continuación, veréis qué otros estados puede tener un proceso.

Estados de un hilo

Un hilo tiene su propio ciclo de vida, durante el cual puede encontrarse en diferentes estados. Java controla el estado de los hilos mediante el llamado planificador de hilos, que se encargará de gestionar qué hilo debe ejecutarse en cada momento y en qué estado deben encontrarse el resto de hilos.

Observad esta imagen.

Ciclo de vida de un proceso

En ella podéis ver un esquema de los diferentes estados en los que puede encontrarse un hilo (representados en un recuadro) y qué métodos pueden llevar ese estado al hilo (representados por flechas).

¿Qué significan cada uno de estos estados? Veámoslo.

  • Nuevo. Se ha creado un objeto hilo, pero todavía no se le ha asignado ninguna tarea. Para ello, se ha de llamar a su método start() y el hilo pasará al estado preparado.
  • Preparado. El hilo está preparado para ejecutarse, pero el planificador de hilos es quién debe decidir si puede hacerlo o debe esperar (por ejemplo, a que acabe la ejecución otro hilo).
  • En ejecución. Una vez el hilo puede acceder a tiempo de CPU, se ejecutará. Si el hilo finaliza su trabajo completamente, pasará al estado muerto. Si el planificador de hilos decide que ha cumplido su periodo de tiempo y el hilo no ha finalizado su trabajo, pasará al estado preparado y esperará a que el planificador de hilos vuelva a darle permiso para ejecutarse.
  • Bloqueado. El hilo no puede ejecutarse porque espera que ocurra algo. En cuanto ocurra lo que le está dejando bloqueado, pasará al estado preparado.
  • Muerto. El hilo ha finalizado su tarea y deja de existir.

Explicaremos en una próxima entrada cómo se gestionan cada uno de estos estados.


Creative Commons License
Concurrencia en Java by Cristian Jorge Garcia Marcos is licensed under a Creative Commons Reconocimiento-Compartir bajo la misma licencia 3.0 España License.

Sincronización de hilos

Publicado: Sábado, 13 \13\UTC junio \13\UTC 2009 en 11. Concurrencia en Java

En la anterior entrada veíamos qué era un hilo, su ciclo de vida y cómo crear uno. En esta entrada estudiaremos algunos de los problemas que existen cuando hay varios hilos en funcionamiento y cómo se solucionan.

Herramientas de planificación de hilos

Java nos proporciona un conjunto de métodos que permiten controlar – en mayor o menor medida – el cambio de un hilo de un estado a otro. A continuación explicaremos uno a uno cada uno de estos métodos.

Método yield()

Un hilo puede ceder el tiempo de CPU que se le ha asignado para que otros hilos puedan ejecutarse. Con la llamada a este método, el hilo pasará del estado en ejecución al estado preparado.

En el caso que no haya otro hilo esperando CPU, el planificador de hilos volverá a cambiar el estado del hilo de preparado a en ejecución, para que vuelva a ocupar tiempo de CPU.

Método sleep(parametroTiempo)

Con la llamada a este método, el hilo pasa al estado bloqueado tantos milisegundos como se le indiquen en el parámetro de entrada parametroTiempo.

Una vez cumplido esa cantidad de tiempo, se pone en estado preparado.

Método join()

Este método permite a un hilo quedar a la espera que termine un segundo hilo.

El método join() suele utilizarse para mantener un orden en la secuencia de los hilos. Así, podemos arrancar una secuencia de hilos llamando a join() para que cada uno finalice en el orden que hemos marcado según la llamada a join().ATENCIÓN

Es obligatorio controlar los métodos join() y sleep() mediante la excepción InterruptedException, para evitar errores de compilación. Por ejemplo, debéis hacer algo como esto:

___________________

public class UnHilo extends Thread {

public UnHilo(String nombreHilo) {

super(nombreHilo);

}

public void run() {

System.out.println(getName());

}

}

___________________

Código de UnHilo.java

___________________

public class TestUnHilo () {

public static void main (String[] args) {

UnHilo hiloUno = new UnHilo(“HiloUno”);

UnHilo hiloDos = new UnHilo(“HiloDos”);

hiloUno.start();

hiloDos.start();

try {

hiloUno.join();

hiloDos.join();

} catch (InterruptedException ie) {

}

System.out.println(“El programa ha finalizado”);

}

}

___________________

Código de TestUnHilo.java con gestión de excepciones

Sección crítica

El problema de la sección crítica es muy conocido en el ámbito del multiprocesamiento. De hecho, posiblemente ya os habréis enfrentado a algo parecido con el estudio de la concurrencia de datos en el módulo de SGBD (qué ocurre cuando se accede a un mismo dato desde varias sesiones simultáneamente).

De todas maneras, abordaremos el problema para aquellos que no lo hayáis dado o, simplemente, se os haya olvidado.

Observad el siguiente código de programa:

_________________

public class Contador {

private int contador = 1;

public void setContador(int nContador) {

contador = nContador;

}

public int getContador() {

return contador;

}

}

_________________

Código de Contador.java

_________________

public class HiloContador extends Thread {

private Contador contador;

public HiloContador(String nNombre, Contador nContador) {

super(nNombre);

contador=nContador;

}

public void run() {

try {

for (int j=0; j<10; j++) {

int i=contador.getContador();

sleep((int) (Math.random()*10));

contador.setContador(i+1);

System.out.println(getName() + ” pone el contador a ” + i);

}

} catch (InterruptedException e) {

System.out.println(“Error al ejecutar el método sleep”);

}

}

}

_________________

Código de HiloContador.java

_________________

public class TestHiloContador {

public static void main(String[] args) {

Contador cont = new Contador();

HiloContador hc1 = new HiloContador(“HiloUno”, cont);

HiloContador hc2 = new HiloContador(“HiloDos”,cont);

hc1.start();

hc2.start();

try{

hc1.join();

hc2.join();

} catch (InterruptedException e) {

System.out.println(“Error al ejecutar el método join”);

}

System.out.println(“El último valor que debería mostrarse es 10*2=20”);

}

}

______________

Código de TestHiloContador.java

Tenemos tres clases: Contador, HiloContador y TestHiloContador.

La clase Contador es la que contendrá la sección crítica, en nuestro caso un sencillo contador. Hemos introducido en la clase un atributo de tipo entero inicializado a 1 y sus correspondientes setter y getter.

La clase HiloContador es el hilo que ejecutará la sección crítica. Fijaos que simplemente se trata de la ejecución de un bucle en 10 iteraciones en el que se incrementa en 1 el valor de la variable contador. Así, finalmente, cada hilo habrá aumentado en 10 unidades el valor inicial que tenia el contador.

IMPORTANTEHemos introducido un tiempo de espera con la instrucción sleep() entre la recogida del valor de contador (getContador()) y su asignación con su incremento en 1 (setContador()) para que la CPU no realice de una sola vez todo un hilo y después otro. Así podremos comprobar el efecto deseado.

La clase TestHiloContador es la que contiene el método main() que creará un objeto Contador y dos hilos HiloContador, que nos permitirá ver el funcionamiento del programa con dos hilos ejecutándose simultáneamente.

Copiad el código y comprobad el resultado. Podréis apreciar que el resultado no es el esperado y que varía cada vez que ejecutamos el programa. Esto se debe a que existe una región crítica (la variable contador) que no controlamos.

Una solución a este problema es utilizar synchronized(nombreDeVariableSeccionCritica) y recoger todo el código de la sección crítica dentro de un bloque. Para entenderlo mejor, fijaos en la solución al problema anterior. Simplemente modificaremos el código de la sección crítica, en nuestro caso, el método run() del hilo. Veámoslo:

public void run() {

try {

synchronized (contador) {

for (int j=0; j<10; j++) {

int i=contador.getContador();

sleep((int) (Math.random()*10));

contador.setContador(i+1);

System.out.println(getName() + ”  pone el contador a ” + i);

}

}

} catch (InterruptedException e) {

System.out.println(“Error al ejecutar el método sleep”);

}

}

Con esta modificación tan sencilla, el resultado siempre será el deseado. La explicación es que ahora el planificador de hilos de Java se encarga de sincronizar que, en cada momento, sólo un hilo pueda acceder al valor de la variable dentro del bloque enmarcado por la palabra reservada synchronized.

Y esto es todo lo que vamos a ver sobre el procesamiento multihilo y la sincronización entre ellos.


Creative Commons License
Sincronización de hilos by Cristian Jorge Garcia Marcos is licensed under a Creative Commons Reconocimiento-Compartir bajo la misma licencia 3.0 España License.