PROGRAMACIÓN CONCURRENTE Se entiende por programación programación concurrente el conjunto de técnicas y notaciones que sirven para expresar el paralelismo potencial en los programas, así como resolver problemas de comunicación comunicación y sincroniz s incronización. ación.
La clase Thread es la clase responsable de producir hilos funcionales para otras clases. Para añadir la funcionalidad de hilo a una clase simplemente se deriva la clase de Thread y se ignora el método run. Es en este método run donde el procesamiento de un hilo toma lugar, ya menudo se refieren a él como el cuerpo del hilo. La clase Thread también define los métodos start y stop, los cuales te permiten comenzar y parar la ejecución del hilo, además de un gran número de métodos útiles.
APLICACIÓN APL ICACIÓN 1 FUNCIONAMIENTO El proceso de cobro de un supermercado; es decir, unos clientes van con un carro lleno de productos y una cajera les cobra los productos, pasándolos pasándolos uno a uno por el escaner de la caja registradora. En este caso la cajera debe de procesar la compra cliente a cliente, es decir que primero le cobra al cliente 1, luego al cliente 2 y así sucesivamente. Para ello vamos a definir una clase “Cajera” y una clase “Cliente” el cual tendrá un “array de enteros” que representaran los productos que ha comprado y el tiempo que la cajera tardará en pasar el producto por el escaner; es decir, que si tenemos un array con [1,3,5] significará significará que el cliente c liente ha comprado 3 productos y que la cajera tardara en procesar el producto 1 ‘1 segundo’, el producto 2 ‘3 segundos’ y el producto 3 en ‘5 segundos’, con lo cual tardara en cobrar al cliente toda su co mpra ‘9 segundos’. segundos’. Clase Cajera.java : “
“
public class Cajera { private String nombre; // Constructor, getter y setter public void procesarCompra(Cliente procesarCompra(Cliente cliente, long t imeStamp) { System.out.println("La System.out.println("La cajera " + this.nombre + " COMIENZA A PROCESAR LA COMPRA DEL CLIENTE " + cliente.getNombre() cliente.getNombre() + " EN EL TIEMPO: " + (System.currentTimeMilli (System.currentTimeMillis() s() - timeStamp) / 1000 + "seg"); for (int i = 0; i < cliente.getCarroCompra().length; cliente.getCarroCompra().length; i++) { this.esperarXsegundos(cli this.esperarXsegundos(cliente.getCarroCompra()[i ente.getCarroCompra()[i]); ]); System.out.println("Procesado System.out.println("Procesado el producto " + (i + 1) + " ->Tiempo: " + (System.currentTimeMilli (System.currentTimeMillis() s() - timeStamp) / 1000 + "seg"); } System.out.println("La System.out.println("La cajera " + this.nombre + " HA TERMINADO DE PROCESAR " + cliente.getNombre() cliente.getNombre() + " EN EL TIEMPO: " + (System.currentTimeMillis() (System.currentTimeMillis() - timeStamp) / 1000 + "seg"); } private void esperarXsegundos(int esperarXsegundos(int segundos) {
try { Thread.sleep(segundos * 1000); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); }}}
Clase “Cliente.java“: public class Cliente { private String nombre; private int[] carroCompra; // Constructor, getter y setter
} Si ejecutásemos este programa propuesto con dos Clientes y con un solo proceso (que es lo que se suele hacer normalmente), se procesaría primero la compra del Cliente 1 y después la del Cliente 2, con lo cual se tardará el tiempo del Cliente 1 + Cliente 2. CUIDADO: Aunque hayamos puesto dos objetos de la clase Cajera (cajera1 y cajera2) no significa que tengamos dos cajeras independientes, lo que estamos diciendo es que dentro del mismo hilo se ejecute primero los métodos de la cajera1 y después los métodos de la cajera2, por tanto a nivel de procesamiento es como si tuviésemos una sola cajera : Clase “Main.java“: public class Main { public static void main(String[] args) { Cliente cliente1 = new Cliente("Cliente 1", new int[] { 2, 2, 1, 5, 2, 3 }); Cliente cliente2 = new Cliente("Cliente 2", new int[] { 1, 3, 5, 1, 1 }); Cajera cajera1 = new Cajera("Cajera 1"); Cajera cajera2 = new Cajera("Cajera 2"); // Tiempo inicial de referencia long initialTime = System.currentTimeMillis(); cajera1.procesarCompra(cliente1, initialTime); cajera2.procesarCompra(cliente2, initialTime); }
APLICACIÓN 2: SEMÁFORO Un hilo produce una salida, que otro hilo usa (consume), sea lo que sea esa salida. Entonces se crea un productor, que será un hilo que irá sacando caracteres por su salida; y se crea también un consumidor que irá recogiendo los caracteres que vaya sacando el productor y un monitor que controlará el proceso de sincronización entre los hilos de ejecución. Funcionará como una tubería, insertando el productor c aracteres en un extremo y leyéndolos el consumidor en el otro, con el monitor siendo la propia tubería.
PRODUCTOR El productor extenderá la clase Thread, y su código es el siguiente: class Productor extends Thread { private Tuberia tuberia; private String alfabeto = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; public Productor( Tuberia t ) { // Mantiene una copia propia del objeto compartido tuberia = t } public void run() { char c; // Mete 10 letras en la tubería for( int i=0; i < 10; i++ ) { c = alfabeto.charAt( (int)(Math.random()*26 ) ); tuberia.lanzar( c ); // Imprime un registro con lo añadido System.out.println( "Lanzado "+c+" a la tuberia." ); // Espera un poco antes de añadir más letras try { sleep( (int)(Math.random() * 100 ) ); } catch( InterruptedException e ) {; }}}}
Notar que se crea una instancia de la clase Tuberia, y que se utiliza el método tuberia.lanzar() para que se vaya construyendo la tubería, en principio de 10 caracteres.
CONSUMIDOR: Ahora se reproduce el código del consumidor, que también extenderá la clase Thread: class Consumidor extends Thread { private Tuberia tuberia; public Consumidor( Tuberia t ) { // Mantiene una copia propia del objeto compartido
tuberia = t; } public void run() { char c; // Consume 10 letras de la tubería for( int i=0; i < 10; i++ ) { c = tuberia.recoger(); // Imprime las letras retiradas System.out.println( "Recogido el caracter "+c ); // Espera un poco antes de coger más letras try { sleep( (int)(Math.random() * 2000 ) ); } catch( InterruptedException e ) {; }}}} En este caso, como en el del productor, se cuenta con un método en la clase Tuberia, tuberia.recoger(), para manejar la información.
MONITOR: Una vez vistos el productor de la información y el consumidor, solamente queda por ver qué es lo que hace la clase Tuberia. Lo que realiza la clase Tuberia, es una función de supervisión de las transacciones entre los dos hilos de ejecución, el productor y el consumidor. class Tuberia { private char buffer[] = new char[6]; private int siguiente = 0; // Flags para saber el estado del buffer private boolean estaLlena = false; private boolean estaVacia = true; // Método para retirar letras del buffer public synchronized char recoger() { // No se puede consumir si el buffer está vacío while( estaVacia == true ) { try { wait(); // Se sale cuando estaVacia cambia a false } catch( InterruptedException e ) { ; }} // Decrementa la cuenta, ya que va a consumir una letra siguiente--; // Comprueba si se retiró la última letra if( siguiente == 0 ) estaVacia = true; // El buffer no puede estar lleno, porque acabamos // de consumir estaLlena = false; notify(); // Devuelve la letra al thread consumidor return( buffer[siguiente] ); } // Método para añadir letras al buffer
public synchronized void lanzar( char c ) { // Espera hasta que haya sitio para otra letra while( estaLlena == true ) { try { wait(); // Se sale cuando estaLlena cambia a false } catch( InterruptedException e ) { ; } } // Añade una letra en el primer lugar disponible buffer[siguiente] = c; // Cambia al siguiente lugar disponible siguiente++; // Comprueba si el buffer está lleno if( siguiente == 6 ) estaLlena = true; estaVacia = false; notify(); } } Ahora que ya se dispone de un productor, un consumidor y un objeto compartido, se necesita una aplicación que arranque los hilos y que consiga que todos hablen con el mismo objeto que es tán compartiendo. class java1007 { public static void main( String args[] ) { Tuberia t = new Tuberia(); Productor p = new Productor( t ); Consumidor c = new Consumidor( t ); p.start(); c.start() ;}}
FUNCIONAMIENTO: Aquí se observa que la variable estaVacia es un semáforo, como los de toda la vida. La naturaleza privada de los datos evita que el productor y el consumidor accedan directamente a éstos. Si se permitiese el acceso directo de ambos hilos de ejecución a los datos, se podrían producir problemas; por ejemplo, si el consumidor intenta retirar datos de un buffer vacío, obtendrá excepciones innecesarias, o se bloqueará el proceso. Los métodos sincronizados de acceso impiden que los productores y consumidores corrompan un objeto compartido. Mientras el productor está añadiendo una letra a la tubería, el consumidor no la puede retirar y viceversa. Esta sincronización es vital para mantener la integridad de cualquier objeto compartido.
APLICACIÓN 3:
Evitar mensajes de tipo ANR - más de 5 segundos.(ANDROID)
package com.seas.threads_handler; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import com.seas.threads_handler.services.ServiceLogin;
public class Threads_Handler extends Acti vity { /*Singleton*/ private static Threads_Handler threads_Handler; publi c static Threads_Handler getInstance(){ return threads_Handler; } /*Fin Singleton*/ private EditText edtEmail; private EditText edtPass; private Button btnEnviar; @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_threads__handler); /*Singleton*/ Threads_Handler.threads_Handler = this; /*Fin Singleton*/ edtEmail = (EditText)findViewById(R.id.edtEmail); edtPass = (EditText)findViewById(R.id.edtPass); btnEnviar = (Button)findViewById(R.id.btnEnviar);
btnEnviar.setOnClickL istener(new OnClickLis tener(){ @Override public void onClick(View arg0) { String email = edtEmail.getText().toString(); String pass = edtPass.getText().toString();
ServiceLogin.accionLogin(email,pass); } }); }
}
FUNCIONAMIENTO: Android controla todas las operaciones que se realizan sobre el “Hilo Principal”, detecta todas aquellas tareas que tengan un coste de más de 5 segundos y las penaliza con el mensaje “Application Not Responding” (ANR). Cuando se muestra este mensaje, el usuario puede decidir forzar el cierre de la aplicación. Esta situación se traduce en una mala experiencia de usuario e induce a pensar que la aplicación no funciona correctamente.
package com.seas.threads_handler.services; import android.app.ProgressDialog; import android.os.Handler; import android.widget.Toast; import com.seas.threads_handler.Threads_Handler;
public class ServiceLogin { private final static Handler manejador = new Ha ndler(); private static String messageUser; private static ProgressDialog dialog;
public static void accionLogin(final String user,final String pass){ Toast.makeText(Threads_Handler.getInstance().getBaseContext(), "Cargando Datos...", Toast.LENGTH_LONG).show(); Thread threadLogin = new Thread(){ public void run(){ try { Thread.sleep(5000); } catch (Exception e) { messageUser = "Error al conectar con el servidor. "; } manejador.post(proceso); } };
threadLogin.start(); }
private final s tatic Runnable proc eso = new Runnable(){ public void run() { try{ Toast.makeText(Threads_Handler.getInstance().getBaseContext(), "Los datos se han cargado correctamente...", Toast.LENGTH_LONG).show(); }catch(Exception e){ Toast.makeText(Threads_Handler.getInstance().getBaseContext(), messageUser, Toast.LENGTH_LONG).show(); } } }; }
Referencias:
Joyanes L. 2001. Java 2: Manual de programación, Primera Edición. Buenos Aires, McGraw-Hill.
Burns A. 2003. Sistemas de tiempo real y lenguajes de programación, Pearson Educación, España