[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 && 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








