Suscribirse al Feed
27Ene

[código] Sincronización de subprocesos - Búfer Circular

En los ejemplos anteriores de subprocesamiento en Java, utilizabamos la sincronización para garantizar que dos hilos manipulen correctamente los datos en un búfer compartido. Sin embargo, es probable que nuestra aplicación no tenga un rendimiento óptimo. Si los dos subprocesos operan a distintas velocidades, es probable que uno de ellos pase la mayor parte del tiempo esperando. Por ejemplo, en los anteriores ejercicios compartiamos una variable entera. Entonces, si por ejemplo el productor realiza sus tareas a una mayor velocidad que el consumidor, este debe esperar al consumidor; y viseversa. Incluso, suponiendo que los subprocesos se ejecutan a la misma velocidad relativa, cabe la posibilidad que uno u otro proceso se retrase, haciendo que se pierda la sincronización entre los mismos.

Como programadores, no debemos hacer suposiciones acerca de las velocidades relativas de los subprocesos concurrentes asíncronos, puesto que existen muchos factores que pueden afectar el tiempo de ejecución de los subprocesos (el sistema operativo, el usuario, el entorno de red, etc.).

El método que vamos a emplear en este ejemplo para minimizar el tiempo de espera que sufren los subprocesos que comparten recursos es crear un búfer circular. Con este nuevo enfoque, en vez de tener una única variable compartida, tendremos un arreglo de búferes. De esta forma, si el productor produce valores a mayor velocidad de la que el consumidor pueda consumirlos, almacenará los valores en posiciones distintas del arreglo (mientras haya espacio). Esto permite al productor hacer su tarea, aún cuando el consumidor no esté listo para realizar la suya. De igual forma, si en ciertas ocaciones el consumidor recoge valores a mayor velocidad de la que el productor pueda consumirlos, este puede buscar otros valores adicionales en el arreglo (si los hay) que el productor pudo haber producido con anterioridad.

Observa además, querido lector, que usar un búfer circular sería inapropiado si los procesos se ejecutan a diferentes velocidades conscientemente. Es decir, si por ejemplo sabemos que el consumidor se ejecuta siempre a mayor velocidad, no necesitariamos espacios adicionales puesto que el productor no tendría tiempo de llenarlas. De igual forma, si el productor se ejecutase siempre a mayor velocidad que el consumidor, necesitariamos un arreglo con infinitas posiciones.

En el ejemplo que veremos en breve (muy similar a los que habíamos visto antes), demuestra cómo un productor y un consumidor utilizan un búfer circular (en este caso un arreglo de tres celdas) mediante la sincronización.

Además, como valor agregado, veremos algo avanzado acerca de las interfaces gráficas. En este ejemplo tanto productor como consumidor (procesos independientes), modifican el contenido de un objeto de interfaz gráfica (en este caso un JTextArea). Puesto que los componentes gráficos de Swing no son "a prueba de subprocesos", cabe la posibilidad de que si diferentes tareas están manipulando el mismo componente los resultados visuales no sean correctos. Todas las interacciones sobre los objetos GUI de Swing deben llevarse a cabo un proceso a la vez. Generalmente, este subproceso es el subproceso despachador de eventos (tambien conocido como el subproceso manejador de eventos). La clase SwingUtilities (del paquete java.swing), proporciona el método estático invokeLater para ayudar a realizar esta tarea. El método invokeLater especifica que las instrucciones de procesamiento de la GUI deben ejecutarse posteriormente, como parte del subproceso despachador de eventos. El método invokeLater recibe un objeto que implemente la clase Runnable (del paquete java.lang). Toda clase que implemente Runnable debe declarar el método run(). Los subprocesos en el siguiente ejemplo, que muestran su salida en la GUI, pasan objetos de la clase SalidaRunnable al método invokeLater. Cada objeto SalidaRunnable contiene una referencia al objeto JTextArea en el que se muestra la salida, y una cadena del mensaje a mostrar. Cuando el programa llama a invokeLater, la actualización del componente GUI se pondrá en la cola para ejecutarse en el subproceso despachador de eventos. Luego, el método run de la clase SalidaRunnable será invocado, haciendo el el componente JTextArea se ejecute actulice de forma segura.

Resultado del ejemplo

Código fuente

La interfaz Bufer.java especifica los métodos llamados por el Productor y el Consumidor:


public interface Bufer {
   public void establecer( int valor );  // colocar valor en Bufer
   public int obtener();              // devolver valor de Bufer
}

La clase SalidaRunnable.java actualiza el objeto JTextArea con los resultados:


import javax.swing.*;
public class SalidaRunnable implements Runnable {
   private JTextArea areaSalida;
   private String mensajeParaAnexar;
   // inicializar areaSalida y mensaje
   public SalidaRunnable( JTextArea salida, String mensaje )
   {
      areaSalida = salida;
      mensajeParaAnexar = mensaje;
   }
   // método llamado por SwingUtilities.invokeLater para actualizar areaSalida
   public void run()
   {
      areaSalida.append( mensajeParaAnexar );
   }
} // fin de la clase SalidaRunnable

El método run de Consumidor.java controla un subproceso que itera diez veces y lee un valor de ubicacionCompartida cada vez

import javax.swing.*;
public class Consumidor extends Thread {
   private Bufer ubicacionCompartida; // referencia al objeto compartido
   private JTextArea areaSalida;
   // constructor
   public Consumidor( Bufer compartido, JTextArea salida )
   {
      super( "Consumidor" );
      ubicacionCompartida = compartido;
      areaSalida = salida;
   }
   // leer el valor de ubicacionCompartida diez veces y sumar los valores
   public void run()
   {
      int suma = 0;
      for ( int cuenta = 1; cuenta <= 10; cuenta++ ) {
         // estar inactivo de 0 a 3 segundos, leer el valor de Bufer y sumarlo a suma
         try {
            Thread.sleep( ( int ) ( Math.random() * 3001 ) );
            suma += ubicacionCompartida.obtener();
         }
         // si se interrumpió el subproceso inactivo, imprimir el rastreo de la pila
         catch ( InterruptedException excepcion ) {
            excepcion.printStackTrace();
         }
      }
      String nombre = getName();
      SwingUtilities.invokeLater( new SalidaRunnable( areaSalida,
         "\nTotal que consumió " + nombre + ": " + suma + ".\n" +
         nombre + " terminado.\n ") );
   } // fin del método run
} // fin de la clase Consumidor

El método run de Productor.java controla un subproceso que almacena valores de 11 a 20 en ubicacionCompartida:

import javax.swing.*;
public class Productor extends Thread {
   private Bufer ubicacionCompartida;
   private JTextArea areaSalida;
   // constructor
   public Productor( Bufer compartido, JTextArea salida )
   {
      super( "Productor" );
      ubicacionCompartida = compartido;
      areaSalida = salida;
   }
   // almacenar valores de 11 a 20 en el búfer de ubicacionCompartida
   public void run()
   {
      for ( int cuenta = 11; cuenta <= 20; cuenta ++ ) {
         // estar inactivo de 0 a 3 segundos, después colocar valor en Bufer
         try {
            Thread.sleep( ( int ) ( Math.random() * 3000 ) );
            ubicacionCompartida.establecer( cuenta );
         }
         // si se interrumpió el subproceso inactivo, imprimir el rastreo de la pila
         catch ( InterruptedException excepcion ) {
            excepcion.printStackTrace();
         }
      }
      String nombre = getName();
      SwingUtilities.invokeLater( new SalidaRunnable( areaSalida, "\n" +
         nombre + " terminó de producir.\n" + nombre + " terminado.\n" ) );
   } // fin del método run
} // fin de la clase Productor

BuferCircular.java sincroniza el acceso a un arreglo de buferes compartidos:

import javax.swing.*;
public class BuferCircular implements Bufer {
   // cada elemento del arreglo es un bufer
   private int buferes[] = { -1, -1, -1 };
   // cuentaBuferesOcupados mantiene la cuenta de buferes ocupados
   private int cuentaBuferesOcupados = 0;
   // variables que mantienen las ubicaciones de lectura y escritura en el bufer
   private int ubicacionLectura = 0, ubicacionEscritura = 0;
   // referencia al componente de la GUI que muestra la salida
   private JTextArea areaSalida;
   // constructor
   public BuferCircular( JTextArea salida )
   {
      areaSalida = salida;
   }
   // colocar valor en bufer
   public synchronized void establecer( int valor )
   {
      // obtener nombre del subproceso que llamó a este método, para mostrarlo en pantalla
      String nombre = Thread.currentThread().getName();
      // mientras no haya ubicaciones vacías, colocar subproceso en estado de espera
      while ( cuentaBuferesOcupados == buferes.length ) {
         // mostrar información de subproceso y de bufer, después esperar
         try {
            SwingUtilities.invokeLater( new SalidaRunnable( areaSalida,
               "\nTodos los buferes llenos. " + nombre + " espera." ) );
            wait();
         }
         // si se interrumpió el proceso en espera, imprimir el rastreo de la pila
         catch ( InterruptedException excepcion )
         {
            excepcion.printStackTrace();
         }
      } // fin de  instrucción while
      // colocar valor en ubicacionEscritura de buferes
      buferes[ ubicacionEscritura ] = valor;
      // actualizar componente de la GUI de Swing con el valor producido
      SwingUtilities.invokeLater( new SalidaRunnable( areaSalida,
         "\n" + nombre + " escribe " + buferes[ ubicacionEscritura ] + " ") );
      // acaba de producir un valor, por lo que se incrementa el número de buferes ocupados
      ++cuentaBuferesOcupados;
      // actualizar ubicacionEscritura para la siguiente operación de escritura
      ubicacionEscritura = ( ubicacionEscritura + 1 ) % buferes.length;
      // mostrar contenido de buferes compartidos
      SwingUtilities.invokeLater( new SalidaRunnable(
         areaSalida, crearSalidaEstado() ) );
      notify(); // regresar el subproceso en espera (si hay uno) al estado listo
   } // fin del método establecer
   // devolver valor de bufer
   public synchronized int obtener()
   {
      // obtener nombre del subproceso que llamó a este método, para mostrarlo en pantalla
      String nombre = Thread.currentThread().getName();
      // mientras no haya datos que leer, colocar el subproceso en estado de espera
      while ( cuentaBuferesOcupados == 0 ) {
         // mostrar información de subproceso y de bufer, después esperar
         try {
            SwingUtilities.invokeLater( new SalidaRunnable( areaSalida,
               "\nTodos los buferes vacios. " + nombre + " espera.") );
            wait();
         }
         // si se interrumpió el subproceso en espera, imprimir el rastreo de la pila
         catch ( InterruptedException excepcion ) {
            excepcion.printStackTrace();
         }
      } // fin de instrucción while
      // obtener valor en ubicacionLectura actual
      int valorLectura = buferes[ ubicacionLectura ];
      // actualizar componente de la GUI de Swing con el valor consumido
      SwingUtilities.invokeLater( new SalidaRunnable( areaSalida,
         "\n" + nombre + " lee " + valorLectura + " ") );
      // acaba de consumir un valor, por lo que se decrementa el número de buferes ocupados
      --cuentaBuferesOcupados;
      // actualizar ubicacionLectura para la siguiente operación de lectura
      ubicacionLectura = ( ubicacionLectura + 1 ) % buferes.length;
      // mostrar contenido de buferes compartidos
      SwingUtilities.invokeLater( new SalidaRunnable(
         areaSalida, crearSalidaEstado() ) );
      notify(); // regresar el subproceso en espera (si hay uno) al estado listo
      return valorLectura;
   } // fin del método obtener
   // crear salida de estado
   public String crearSalidaEstado()
   {
      // primera línea de información de estado
      String salida =
         "(buferes ocupados: " + cuentaBuferesOcupados + ")\nbuferes: ";
      for ( int i = 0; i < buferes.length; i++ )
         salida += " " + buferes[ i ] + "  ";
      // segunda línea de información de estado
      salida += "\n         ";
      for ( int i = 0; i < buferes.length; i++ )
         salida += "---- ";
      // tercera línea de información de estado
      salida += "\n         ";
      // anexar indicadores de ubicacionLectura (L) y ubicacionEscritura (E)
      // debajo de las ubicaciones de bufer apropiadas
      for ( int i = 0; i < buferes.length; i++ )
         if ( i == ubicacionEscritura &amp;&amp; ubicacionEscritura == ubicacionLectura )
            salida += " EL  ";
         else if ( i == ubicacionEscritura )
            salida += " E   ";
         else if ( i == ubicacionLectura )
            salida += "  L  ";
         else
            salida += "     ";
      salida += "\n";
      return salida;
   } // fin del método crearSalidaEstado
} // fin de la clase BuferCircular

PruebaBuferCircular.java muestra a dos subprocesos manipulando un búfer circular:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
// establecer los subprocesos productor y consumidor e iniciarlos
public class PruebaBuferCircular extends JFrame {
   JTextArea areaSalida;
   // configurar la GUI
   public PruebaBuferCircular()
   {
      super( "Demostracion de sincronizacion de subprocesos" );
      areaSalida = new JTextArea( 20,30 );
      areaSalida.setFont( new Font( "Monospaced", Font.PLAIN, 12 ) );
      getContentPane().add( new JScrollPane( areaSalida ) );
      setSize( 350, 500 );
      setVisible( true );
      // crear objeto compartido utilizado por los subprocesos; utilizamos una referencia
      // BuferCircular en vez de una referencia Bufer, para poder invocar al
      // método crearSalidaEstado de BuferCircular
      BuferCircular ubicacionCompartida = new BuferCircular( areaSalida );
      // mostrar el estado inicial de los búferes en BuferCircular
      SwingUtilities.invokeLater( new SalidaRunnable( areaSalida,
         ubicacionCompartida.crearSalidaEstado() ) );
      // establecer subprocesos
      Productor productor = new Productor( ubicacionCompartida, areaSalida );
      Consumidor consumidor = new Consumidor( ubicacionCompartida, areaSalida );
      productor.start();  // iniciar subproceso productor
      consumidor.start();  // iniciar subproceso consumidor
   } // fin del constructor
   public static void main ( String args[] )
   {
      PruebaBuferCircular aplicacion = new PruebaBuferCircular();
      aplicacion.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
   }
} // fin de la clase PruebaBuferCircular

Descargar código fuente

Los ejercicios utilizados en este post están basados en ejemplos del libro Cómo programar en Java de Deitel, y por lo tanto están bajo la licencia que esta editorial disponga.

 

 

Antes de comentar... recuerda que no hago tareas

Te invito a subscribirte al feed RSS. ¿No sabes que es un lector de Feeds?

© 2007 - 2008 Dezinerfolio. Todos los derechos reservados.
Powered by Wordpress. Entradas RSS