G.Bordel >Docencia >TAP Técnicas Actuales de Programación (curso 2010-2011)
desprotegido Intro. desprotegido Temario desprotegido Calendario desprotegido RPF desprotegido Recursos protegido Práctica protegido Gest. Alum.
tema_anterior Tema 10: Hilos tema_siguiente
  1. Introducción.
  2. Ciclo de vida de un hilo.
  3. Distribución de la CPU. Prioridades. El problema de la "inanición".
  4. Mecanismos de sincronización de hilos. El problema del interbloqueo.
  5. Agrupamientos de hilos.
  6. Estudio de un ejemplo de programación con hilos.[ejercicios]

10.6- Estudio de un ejemplo de programación con hilos

Plantearemos a continuación un proceso muy corriente en aplicaciones software que es el de procesamiento de los datos que produce una determinada fuente (p.ej.un canal de comunicaciones). En general podemos plantear la situación en que los datos proceden de varias fuentes y son procesados por varios hilos semejantes.

En este ejemplo implementaremos por nosotros mismos el mecanismo de exclusión (el semáforo) sobre un recurso compartido que será una variable sobre la que los productores de datos situan uno cada vez y de la que los consumidores lo recogen. Por tanto generaremos un objeto con espacio para el dato (sea un entero en esta simulación) más la variable semáforo.

1-  public class Cubiculo {
2-              private int contenido;
3-              private boolean disponible = false;
4-  
5-              public synchronized int get() {
6-                  ...
7-              }
8-  
9-              public synchronized void put(int valor) {
10-                  ...
11-              }
12-          }

Un objeto de nuestra clase "Cubiculo" será el elemento que podrá tomarse en exclusividad. La variable "contenido" recibirá enteros de los hilos productores de datos para que posteriormente sean recogidos por los hilos consumidores de datos. El valor de la variable "disponible" será "cierto" cuando un productor haya situado un valor en "contenido" y aun no haya sido leido por un consumidor. Su valor será "falso" cuando el dato no sea válido: inicialmente cuando aún no se ha producido ninguno y posteriormente cuando un consumidor lo lea. El acceso y la gestión de estas variables se hará mediante la rutina de escritura "put" que usarán los productores, y la de lectura "get" que usarán los consumidores. Es en estas rutinas donde se consigue el sincronismo ya que se definen como sincronizadas de modo que nunca podrán ser ejecutadas a la vez por más de un hilo (esto tiene sus matizaciones como veremos más adelante).

El método put espera a que no haya un dato disponible en el cubículo de modo que pueda situarse uno. Para ello establece el ciclo en las líneas 2-5 donde realiza una espera ( wait en 4) de la que saldrá al recibir una notificación (cuando se ejecute un notify o notifyAll sobre el objeto). Una vez que encuentra el cubículo dispuesto a recibir un dato pasa a situarlo en la variable "contenido" y a indicar su disponibilidad en la variable "disponible". Una vez hecho esto notifica a todo hilo que este esperando un acontecimiento en el objeto que "algo" ha sucedido (notifyAll en 9).

Aquí debemos matizar lo que se decía antes: efectivamente esta rutina se ejecuta tomando el objeto en propiedad y por tanto impidiendo que otros hilos actuen sobre él, pero al ejecutar el wait se espera una notificación por parte de otro hilo de que "algo" ha cambiado en el cubículo, es decir, otro hilo debe actuar sobre el cubículo. Esto es posible porque el método wait cede el permiso de acceso al objeto aún cuando nos encontremos dentro de una rutina sincronizada.

1-  public synchronized void put(int valor) {
2-    while (disponible) {
3-      // esperar a que el consumidor recoja un valor
4-      try { wait(); } catch (InterruptedException e) {}
5-    }
6-    contenido = valor;
7-    disponible = true;
8-    // notificar al consumidor que el valor ha sido generado
9-    notifyAll();
10-  } 

El método get es en cierto modo "simétrico" al put. Aquí la espera lo es a la condición contraria, ya que se pretende tomar un dato del objeto. Cuando se recoge se marca que ya no es válido (que el espacio esta de nuevo disponible para un nuevo dato) y se notifica a todos los hilos que esten a la espera.

Como se puede ver, estos dos métodos no reciben una notificación específica que les permita salir del wait y avanzar, sino que ha de establecerse el ciclo de comprobación de la variable "disponible" porque las notificaciones son generales y los hilos actuan "en competencia". Es decir, todos los hilos productores y consumidores se "despertarán" cada vez que se produzca un cambio en el cubículo, pero sólo uno de ellos podrá ejecutar una acción de modo que el resto volverá a "caer" en un wait

1-  public synchronized int get() {
2-    while (!disponible) {
3-     // esperar a que el productor genere un valor
4-      try { wait();} catch (InterruptedException e) {}
5-    }
6-    disponible = false;
7-    // notificar al productor que el valor ha sido recogido
8-    notifyAll();
9-    return contenido;
10-    }

Una vez que disponemos del cubículo podemos escribir las clases de los productores y los consumidores. Los productores serán símplemente objetos que generarán números enteros en un ciclo de 0 a 9 con una cadencia temporal aleatoria para lo cual escribimos una clase descendiente de Thread cuyo método run es un "for" (lineas 10-16) para realizar un put del índice, seguido de una espera mediante un sleep de una cantidad aleatoria de tiempo con un máximo de 100 milisegundos (linea 14). En la linea 12 se muestra por consola una línea de texto indicando que el productor (identificado por un valor entero que se le ha proporcionado en el constructor) ha situado el número correspondiente.

1-  public class Productor extends Thread {
2-              private Cubiculo cubiculo;
3-              private int numeroDeProductor;
4-  
5-              public Productor(Cubiculo c, int numeroDeProductor) {
6-                  cubiculo = c; this.numeroDeProductor = numeroDeProductor;
7-              }
8-  
9-              public void run() {
10-                  for (int i = 0; i < 10; i++) {
11-                      cubiculo.put(i);
12-                      System.out.println("("+numeroDeProductor+ ") >> " + i);
13-                      try {
14-                          sleep((int)(Math.random() * 100));
15-                      } catch (InterruptedException e) { }
16-                  }
17-              }
18-          }

La clase que define los consumidores tiene un aspecto "semejante" como puede verse a continuación. Como el ciclo del run en este caso no supone ninguna espera se ha introducido un yield para no bloquear la CPU en sistemas que no dispongan de un distribuidor preemptivo. Una diferencia esencial con el caso de los productores es que los hilos de este tipo serán "daemons" (el ciclo del run es infinito), lo que se declara en la línea 7 para que el sistema los elimine cuando tan hayan terminado los demás hilos.

Del mismo modo que en el caso del productor hemos introducido una línea (la 14) en la que se informa por la salida estándar de la acción realizada por los objetos de esta clase.

1-  public class Consumidor extends Thread {
2-      private Cubiculo cubiculo;
3-      private int numeroDeConsumidor;
4-  
5-      public Consumidor(Cubiculo c, int numero) {
6-          cubiculo = c; this.numeroDeConsumidor = numeroDeConsumidor;
7-          setDaemon(true);
8-      }
9-  
10-      public void run() {
11-          int valor = 0;
12-          while (true) {
13-              valor = cubiculo.get();
14-              System.out.println("  (" + numeroDeConsumidor+ ") << " + valor);
15-              yield();
16-          }
17-      }
18-  }

Ya sólo nos queda comprobar el funcionamiento de estas clases mediante un pequeño programa principal que se limita a instanciar y poner en marcha un conjunto de productores y consumidores (tres de cada en el ejemplo) que comparten un solo cubículo.

1-  public class MainProdCons extends Object {
2-  
3-    public static void main (String args[]) {
4-      Cubiculo cubiculo=new Cubiculo();
5-      Productor p1=new Productor(cubiculo,1);
6-      Productor p2=new Productor(cubiculo,2);
7-      Productor p3=new Productor(cubiculo,3);
8-      Consumidor c1=new Consumidor(cubiculo,1);
9-      Consumidor c2=new Consumidor(cubiculo,2);
10-      Consumidor c3=new Consumidor(cubiculo,3);
11-      
12-      p1.start();
13-      p2.start();
14-      p3.start();
15-      c1.start();
16-      c2.start();
17-      c3.start();
18-      
19-      }
20-  
21-  }

A continuación puede verse el resultado de una ejecución del anterior programa. Obsérvese cómo se suceden las acciones con cierto detenimiento para entender cómo se está llevando a cabo la distribución de CPU entre los distintos hilos.

Se advierte una anomalía en este resultado en las líneas 50-53 ya que "da la sensación" de que disponemos de dos cubículos al introducirse dos valores seguidos y ser recuperados a continuación sin problemas. Queda como tarea del alumno dar una explicación a este fenómeno.

1-  >java MainProdCons
2-  (1) >> 0
3-    (1) << 0
4-  (2) >> 0
5-    (2) << 0
6-  (3) >> 0
7-    (3) << 0
8-  (2) >> 1
9-    (1) << 1
10-  (3) >> 1
11-    (2) << 1
12-  (1) >> 1
13-    (3) << 1
14-  (1) >> 2
15-    (1) << 2
16-  (3) >> 2
17-    (2) << 2
18-  (2) >> 2
19-    (3) << 2
20-  (3) >> 3
21-    (1) << 3
22-  (1) >> 3
23-    (2) << 3
24-  (3) >> 4
25-    (3) << 4
26-  (2) >> 3
27-    (1) << 3
28-  (1) >> 4
29-    (2) << 4
30-  (2) >> 4
31-    (3) << 4
32-  (3) >> 5
33-    (1) << 5
34-  (2) >> 5
35-    (2) << 5
36-  (3) >> 6
37-    (3) << 6
38-  (1) >> 5
39-    (1) << 5
40-  (2) >> 6
41-    (2) << 6
42-  (3) >> 7
43-    (3) << 7
44-  (2) >> 7
45-    (1) << 7
46-  (1) >> 6
47-    (2) << 6
48-  (3) >> 8
49-    (3) << 8
50-  (2) >> 8
51-  (1) >> 7
52-    (1) << 8
53-   (2) << 7
54-  (3) >> 9
55-    (3) << 9
56-  (2) >> 9
57-    (1) << 9
58-  (1) >> 8
59-    (2) << 8
60-  (1) >> 9
61-    (3) << 9

Siguiente Tema: 11- Programacion en red


Plataforma de soporte a curso y contenidos (c) German Bordel 2005.