Ir al contenido principal

UNIDAD 4. PROGRAMACIÓN CONCURRENTE MULTIHILOS

 4.1. CONCEPTO DE HILOS
Un hilo es una unidad básica de utilización de CPU, la cual contiene un id de hilo, su propio program counter, un conjunto de registros, y una pila; que se representa a nivel del sistema operativo con una estructura llamada TCB (thread control block). Los hilos comparten con otros hilos que pertenecen al mismo proceso la sección de código, la sección de datos, entre otras cosas. Si un proceso tiene múltiples hilos, puede realizar más de una tarea a la vez (esto es real cuando se posee más de un CPU). 
Veamos un ejemplo para clarificar el concepto: Un servidor web acepta solicitudes de los clientes que piden páginas web. Si este servidor tiene varios clientes y funcionara con un solo hilo de ejecución, solo podría dar servicio a un cliente por vez, y el tiempo que podría esperar un cliente para ser atendido podría ser muy grande. Una posible solución sería que el servidor funcione de tal manera que acepte una solicitud por vez, y que cuando reciba otra solicitud, cree otro proceso para dar servicio a la nueva solicitud. Pero crear un proceso lleva tiempo y utiliza muchos recursos, entonces, si cada proceso realizará las mismas tareas ¿Por qué no utilizar hilos? Generalmente es más eficiente usar un proceso que utilice múltiples hilos (un hilo para escuchar las solicitudes, y cuando llega una solicitud, el lugar de crear otro proceso, se crea otro hilo para procesar la solicitud).

4.2. COMPARACIÓN DE UN PROGRAMA DE FLUJO ÚNICO CONTRA UN FLUJO MÚLTIPLE

Programas de flujo único
Un programa de flujo único o mono-hilvanado (single-thread) utiliza un único flujo de control (thread) para controlar su ejecución. Muchos programas no necesitan la potencia o utilidad de múltiples flujos de control. Sin necesidad de especificar explícitamente que se quiere un único flujo de control, muchos de los applets y aplicaciones son de flujo único.
Por ejemplo:
public class Hola Mundo {
    static public void main( String args[] ) {

        System.out.println( “Hola Mundo!” );

        }

    }
Aquí, cuando se llama a main(), la aplicación imprime el mensaje y termina. Esto ocurre dentro de un único hilo de ejecución (thread).
Debido a que la mayor parte de los entornos operativos no solían ofrecer un soporte razonable para múltiples hilos de control, los lenguajes de programación tradicionales, tales como C++, no incorporaron mecanismos para describir de manera elegante situaciones de este tipo. La sincronización entre las múltiples partes de un programa se llevaba a cabo mediante un bucle de suceso único. Estos entornos son de tipo síncrono, gestionados por sucesos. Entornos tales como el de Macintosh de Apple, Windows de Microsoft y X11/Motif fueron diseñados en torno al modelo de bucle de suceso.
Programas de flujo múltiple
En la aplicación de saludo, no se ve el hilo de ejecución que corre el programa. Sin embargo, Java posibilita la creación y control de hilos de ejecución explícitamente. La utilización de hilos (threads) en Java, permite una enorme flexibilidad a los programadores a la hora de plantearse el desarrollo de aplicaciones. La simplicidad para crear, configurar y ejecutar hilos de ejecución, permite que se puedan implementar muy poderosas y portables aplicaciones/applets que no se puede con otros lenguajes de tercera generación. En un lenguaje orientado a Internet como es Java, esta herramienta es vital.
Si se ha utilizado un navegador con soporte Java, ya se habrá visto el uso de múltiples hilos en Java. Habrá observado que dos applets se pueden ejecutar al mismo tiempo, o que puede desplazar la página del navegador mientras el applet continúa ejecutándose. Esto no significa que el applet utilice múltiples hiloss, sino que el navegador es multihilo, multihilvanado o multithreaded.
Los navegadores utilizan diferentes hilos ejecutándose en paralelo para realizar varias tareas, “aparentemente” concurrentemente. Por ejemplo, en muchas páginas web, se puede desplazar la página e ir leyendo el texto antes de que todas las imágenes estén presentes en la pantalla. En este caso, el navegador está trayéndose las imágenes en un hilo de ejecución y soportando el desplazamiento de la página en otro hilo diferente.
Las aplicaciones (y applets) multihilo utilizan muchos contextos de ejecución para cumplir su trabajo. Hacen uso del hecho de que muchas tareas contienen subtareas distintas e independientes. Se puede utilizar un hilo de ejecución para cada subtarea.
Mientras que los programas de flujo único pueden realizar su tarea ejecutando las subtareas secuencialmente, un programa multihilo permite que cada thread comience y termine tan pronto como sea posible. Este comportamiento presenta una mejor respuesta a la entrada en tiempo real.
Vamos a modificar el programa de saludo creando tres hilos de ejecución individuales, que imprimen cada uno de ellos su propio mensaje de saludo, Multi Hola.java:
// Definimos unos sencillos hilos. Se detendrán un rato
// antes de imprimir sus nombres y retardos
class Test Th extends Thread {
    private String nombre;

    private int retardo;



    // Constructor para almacenar nuestro nombre

    // y el retardo

    public Test Th( String s,int d ) {

        nombre = s;

        retardo = d;

        }



    // El metodo run() es similar al main(), pero para

    // threads. Cuando run() termina el thread muere

    public void run() {

        // Retasamos la ejecución el tiempo especificado

        try {

            sleep( retardo );

        } catch( Interrupted Exception e ) {

            ;

        }



        // Ahora imprimimos el nombre

        System.out.println( “Hola Mundo! “+nombre+” “+retardo );

        }

    }
public class Multi Hola {
    public static void main( String args[] ) {

        Test Th t1,t2,t3;



        // Creamos los threads

        t1 = new Test Th( “Thread 1″,(int)(Math.random()*2000) );

        t2 = new Test Th( “Thread 2″,(int)(Math.random()*2000) );

        t3 = new Test Th( “Thread 3″,(int)(Math.random()*2000) );



        // Arrancamos los threads

        t1.start();

        t2.start();

        t3.start();

        }

    }

4.3. CREACIÓN Y CONTROL DE HILOS

Creación de un Thread
Hay dos modos de conseguir threads en Java. Una es implementando la interface Runnable, la otra es extender la clase Thread.


La implementación de la interface Runnable es la forma habitual de crear threads. Las interfaces proporcionan al programador una forma de agrupar el trabajo de infraestructura de una clase. Se utilizan para diseñar los requerimientos comunes al conjunto de clases a implementar. La interface define el trabajo y la clase, o clases, que implementan la interface realizan ese trabajo. Los diferentes grupos de clases que implementen la interface tendrán que seguir las mismas reglas de funcionamiento.

Hay una cuantas diferencias entre interface y clase. Primero, una interface solamente puede contener métodos abstractos y/o variables estáticas y finales (constantes). Las clases, por otro lado, pueden implementar métodos y contener variables que no sean constantes. Segundo, una interface no puede implementar cualquier método. Una clase que implemente una interface debe implementar todos los métodos definidos en esa interface. Una interface tiene la posibilidad de poder extenderse de otras interfaces y, al contrario que las clases, puede extenderse de múltiples interfaces. Además, una interface no puede ser instanciada con el operador new; por ejemplo, la siguiente sentencia no está permitida:Runnable a = new Runnable(); // No se permite
El primer método de crear un thread es simplemente extender la clase Thread:class MiThread extends Thread { public void run() {
}
. . .

El ejemplo anterior crea una nueva clase MiThread que extiende la clase Thread y sobrecarga el método Thread.run() por su propia implementación. El método run() es donde se realizará todo el trabajo de la clase. Extendiendo la clase Thread, se pueden heredar los métodos y variables de la clase padre. En este caso, solamente se puede extender o derivar una vez de la clase padre. Esta limitación de Java puede ser superada a través de la implementación de Runnable:
public class MiThread implements Runnable {
Thread t; public void run() {
// Ejecución del thread una vez creado
}
}
En este caso necesitamos crear una instancia de Thread antes de que el sistema pueda ejecutar el proceso como un thread. Además, el método abstracto run() está definido en la interface Runnable tiene que ser implementado. La única diferencia entre los dos métodos es que este último es mucho más flexible. En el ejemplo anterior, todavía tenemos oportunidad de extender la clase MiThread, si fuese necesario. La mayoría de las clases creadas que necesiten ejecutarse como un thread , implementarán la interface Runnable, ya que probablemente extenderán alguna de su funcionalidad a otras clases.
No pensar que la interface Runnable está haciendo alguna cosa cuando la tarea se está ejecutando. Solamente contiene métodos abstractos, con lo cual es una clase para dar idea sobre el diseño de la clase Thread. De hecho, si vemos los fuentes de Java, podremos comprobar que solamente contiene un método abstracto:
package java.lang;
public interface Runnable {
public abstract void run() ;
}
Y esto es todo lo que hay sobre la interface Runnable. Como se ve, una interface sólo proporciona un diseño para las clases que vayan a ser implementadas. En el caso de Runnable, fuerza a la definición del método run(), por lo tanto, la mayor parte del trabajo se hace en la clase Thread. Un vistazo un poco más profundo a la definición de la clase Thread nos da idea de lo que realmente está pasando:
public class Thread implements Runnable {
... public void run() {
tarea.run() ;
if( tarea != null ) } } ...
}
De este trocito de código se desprende que la clase Thread también implemente la interface Runnable. tarea.run() se asegura de que la clase con que trabaja (la clase que va a ejecutarse como un thread) no sea nula y ejecuta el método run() de esa clase. Cuando esto suceda, el método run() de la clase hará que corra como un thread.

4.4. SINCRONIZACION DE HILOS

Las lecciones anteriores contenían ejemplos con threads asíncronos e independientes. Esto es, cada thread contenía todos los datos y métodos necesarios y no requerian recursos externos. Además, los threads de esos ejemplos se ejecutaban en su propio espacio sin concernir sobre el estado o actividad de otros threads que se ejecutaban de forma concurrente.

Sin embargo, existen muchas situaciones interesantes donde ejecutar threads concurrentes que compartan datos y deban considerar el estado y actividad de otros threads. Este conjunto de situaciones de programación son conocidos como escenarios 'productor/consumidor'; donde el productor genera un canal de datos que es consumido por el consumidor.

Por ejemplo, puedes imaginar una aplicación Java donde un thread (el productor) escribe datos en un fichero mientras que un segundo thread (el consumidor) lee los datos del mismo fichero. O si tecleas caracteres en el teclado, el thread productor situa las pulsaciones en una pila de eventos y el thread consumidor lee los eventos de la misma pila. Estos dos ejemplos utilizan threads concurrentes que comparten un recurso común; el primero comparte un fichero y el segundo una pila de eventos.

Como los threads comparten un recurso común, deben sincronizarse de alguna forma.

Esta lección enseña la sincronización de threads Java mediante un sencillo ejemplo de productor/consumidor.
EJEMPLO:
El Productor genera un entero entre 0 y 9 (inclusive), lo almacena en un objeto "CubbyHole", e imprime el número generado. Para hacer más interesante el problema de la sincronización, el prodcutor duerme durante un tiempo aleatorio entre 0 y 100 milisegundos antes de repetir el ciclo de generación de números.

class Producer extends Thread {

    private CubbyHole cubbyhole;

    private int number;


    public Producer(CubbyHole c, int number) {

        cubbyhole = c;

        this.number = number;

    }


    public void run() {

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

            cubbyhole.put(i);

            System.out.println("Productor #" + this.number + " pone: " + i);

            try {

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

            } catch (InterruptedException e) {

            }

        }

    }

}

El Consumidor, estándo hambriento, consume todos los enteros de CubbyHole
(exactamenten el mismo objeto en que el productor puso los enteros en primer lugar)
tan rápidamente como estén disponibles.

class Consumer extends Thread {

    private CubbyHole cubbyhole;

    private int number;


    public Consumer(CubbyHole c, int number) {

        cubbyhole = c;

        this.number = number;

    }
    public void run() {

        int value = 0;

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

            value = cubbyhole.get();

            System.out.println("Consumidor #" + this.number + " obtiene: " + value);

        }

    }

}

En este ejemplo el Productor y el Consumidor comparten datos a través de un objeto CubbyHole común. Observaráa que ninguno de los dos hace ningún esfuerzo sea el que sea para asegurarse de que el consumidor obtiene cada valor producido una y sólo una vez. La sincronización entre estos dos threads realmente ocurre a un nivel inferior, dentro de los métodos get() y put() del objeto CubbyHole. Sin embargo, asumamos por un momento que estos dos threads no están sincronizados y veamos los problemas potenciales que podría provocar esta situación.

Un problema sería cuando el Productor fuera más rápido que el Consumidor y generara dos números antes de que el Consumidor tuviera una posibilidad de consumir el primer número. Así el Consumidor se saltaría un número. Parte de la salida se podría parecer a esto.

    . . .

Consumidor #1 obtiene: 3

Productor #1 pone: 4

Productor #1 pone: 5

Consumidor #1 obtiene: 5

    . . .

Otro problema podría aparecer si el consumidor fuera más rápido que el Productor y consumiera el mismo valor dos o más veces. En esta situación el Consumidor imprimirá el mismo valor dos veces y podría producir una salida como esta.

    . . .

Productor #1 pone: 4

Consumidor #1 obtiene: 4

Consumidor #1 obtiene: 4

Productor #1 pone: 5

    . . .

De cualquier forma, el resultado es erróneo. Se quiere que el consumidor obtenga cada entero producido por el Productor y sólo una vez. Los problemas como los escritos anteriormente,se llaman condiciones de carrera. Se alcanzan cuando varios threads ejecutados asíncronamente intentan acceder a un mismo objeto al mismo tiempo y obtienen resultados erróneos.

Para prevenir estas condiciones en nuestro ejemplo Productor/Consumidor, el almacenamiento de un nuevo entero en CubbyHole por el Productor debe estar sincronizado con la recuperación del entero por parte del Consumidor. El Consumidor debe consumir cada entero exactamente una vez. El programa Productor/Consumidor utiliza dos mecanismos diferentes para sincronizar los threads Producer y Consumer; los monitores, y los métodos notify() y wait().

Los objetos, como el CubbyHole que son compartidos entre dos threads y cuyo acceso debe ser sincronizado son llamados condiciones variables. El lenguaje Java permite sincronizar threads alrededor de una condición variable mediante el uso de monitores. Los monitores previenen que dos threads accedan simultáneamente a la misma variable.
En un nivel superior, el ejemplo Productor/Consumidor utiliza los métodos notify() y wait() del objeto para coordinar la activadad de los dos threads. El objeto CubyHole utiliza notify() y wait() para asegurarse de que cada valor situado en él por el Productor es recuperado una vez y sólo una por el Consumidor.

. El programa Principal

Aquí tienes una pequeña aplicación Java que crea un objeto CubbyHole, un Producer, un Consumer y arranca los dos threads.

class ProducerConsumerTest {

    public static void main(String[] args) {

        CubbyHole c = new CubbyHole();

        Producer p1 = new Producer(c, 1);

        Consumer c1 = new Consumer(c, 1);


        p1.start();

        c1.start();

    }

}

. La Salida

Aquí tienes la salida del programa ProducerConsumerTest.

Producer #1 pone: 0

Consumidor #1 obtiene: 0

Productor #1 pone: 1

Consumidor #1 obtiene: 1

Productor #1 pone: 2

Consumidor #1 obtiene: 2

Productor #1 pone: 3

Consumidor #1 obtiene: 3

Productor #1 pone: 4

Consumidor #1 obtiene: 4

Productor #1 pone: 5

Consumidor #1 obtiene: 5

Productor #1 pone: 6

Consumidor #1 obtiene: 6

Productor #1 pone: 7

Consumidor #1 obtiene: 7

Productor #1 pone: 8

Consumidor #1 obtiene: 8

Productor #1 pone: 9

Consumidor #1 obtiene: 9



Monitores Java

El lenguaje Java y el sistema de ejecución soportan la sincronizaxión de threads mediante el uso de monitores. En general, un monitor está asociado con un objeto especifico (una condición variable) y funciona como un bloqueo para ese dato. Cuando un thread mantiene el monitor para algún dato del objeto, los otros threads están bloqueados y no pueden ni inspeccionar ni modificar el dato.

Los segmentos de código dentro de programa que acceden al mismo dato dentro de threads concurrentes separados son conocidos como secciones críticas. En el lenguaje Java, se pueden marcar las secciones críticas del programa con la palabra clave synchronized.

Nota: Generalmente, la sección críticas en los programas Java son métodos. Se pueden marcar segmentos pequeños de código como sincronizados.

Sin embargo, esto viola los paradigmas de la programación orientada a objetos y produce un código que es díficil de leer y de mantener. Para la mayoría de los propósitos de programación en Java, es mejor utilizar synchronized sólo a nivel de métodos.

En el lenguaje Java se asocia un único monitor con cada objeto que tiene un método sincronizado. La clase CubbyHole del ejemplo Producer/Consumer de la página anterior tiene dos métodos sincronizados: el método put(), que se utiliza para cambiar el valor de CubbyHole, y el método get(), que se utiliza para el recuperar el valor actual. Así el sistema asocia un único monitor con cada ejemplar de CubbyHole.

Aquí tienes el código fuente del objeto CubbyHole. Las líneas en negrita proporcionan la sincronización de los threads.

class CubbyHole {

    private int contents;

    private boolean available = false;


    public synchronized int get() {

        while (available == false) {

            try {

                wait();

            } catch (InterruptedException e) {

            }

        }

        available = false;

        notify();

        return contents;

    }


    public synchronized void put(int value) {

        while (available == true) {

            try {

                wait();

            } catch (InterruptedException e) {

            }

        }

        contents = value;

        available = true;

        notify();

    }

}

La clase CubbyHole tiene dos variables privadas: contents, que es el contenido actual de CubbyHole, y la variable booleana available, que indica si se puede recuperar el contenido de CubbyHole. Cuando available es verdadera indica que el Productor ha puesto un nuevo valor en CubbyHole y que el Consumidor todavía no la ha consumido. El Consumidor sólo puede consumir el valor de CubbyHole cuando available es verdadera.

Como CubbyHole tiene dos métodos sincronizados, java proporciona un único monitor para cada ejemplar de CubbyHole (incluyendo el compartido por el Productor y el Consumidor). Siempre que el control entra en un método sincronizado, el thread que ha llamado el método adquiere el monitor del objeto cuyo método ha sido llamado. Otros threads no pueden llamar a un método sincronizado del mismo objeto hasta que el monitor sea liberado.
BIBLIOGRÁFIA:
Fundamentos de Programación en Java
Autor: Schildt Herbert

INTEGRANTES:

JOSE IVAN VELASQUEZ LOPEZ
MIRIAM ANALI FLORES DAVILA
DANIELA MICHEL ZEPEDA DIAZ



Comentarios

Publicar un comentario

Entradas populares de este blog

UNIDAD 5. PROGRAMACIÓN DE DISPOSITIVOS MÓVILES

5.1. INTRODUCCIÓN  La generalización en los últimos años de teléfonos móviles, smartphones, PDs, etc. ha generado una importante necesidad de aplicaciones para este tipo de dispositivos. Las prestaciones de los dispositivos móviles se incrementan día a día, posibilit ando la implementación de aplicaciones muy interesantes tales como: * Acceso a Internet y conectividad mediante Bluetooth * Envío de mensajes cortos. 5.2. DISPOSITIVOS MÓVILES Un dispositivo móvil es un pequeño dispositivo de computación portátil que generalmente incluye una pantalla y un método de entrada (ya sea táctil o teclado en miniatura). Muchos dispositivos móviles tienen sistemas operativos que pueden ejecutar aplicaciones. Las aplicaciones hacen posible para los dispositivos móviles y teléfonos celulares se utilicen como dispositivos para juegos, reproductores multimedia, calculadoras, navegadores y más. Los sistemas operativos y fabricantes más conocidos son: Android. Apple (i...