miércoles, 25 de abril de 2012

PROGRAMACIÓN CONCURRENTE MULTIHILO



Concepto de hilo.

   Los hilos o threads, son básicamente, pequeños procesos o piezas independientes de un gran proceso. También podemos decir, que un hilo es un flujo único de ejecución dentro de un proceso (un proceso es un programa ejecutándose dentro de su propio espacio de direcciones).

   Un hilo no puede correr por sí mismo, se ejecuta dentro de un programa, ya que requieren la supervisión de un proceso padre para correr.  Se pueden porgramar múltiples hilos de ejecución para que corran simultáneamente en el mismo programa. La utilidad de la programación multihilo resulta evidente. Por ejemplo, un navegador Web puede descargar un archivo de un sitio, y acceder a otro sitio al mismo tiempo. Si el navegador puede realizar simultáneamente dos tareas, no tendrá que esperar hasta que el archivo haya terminado de descargarse para poder navegar a otro sitio.

   Los hilos a menudo, son conocidos o llamados procesos ligeros. 

Comparación.

   Un thread o hilo es, al igual que un proceso, un flujo de control que puede gozar de cierta autonomía (puede tener sus propias estructuras de datos), pero a diferencia de un proceso, diversos hilos dentro de una aplicación pueden compartir los mismos datos.

   El beneficio de ser multihilo, consiste en un mejor rendimiento interactivo y un mejor comportamiento en tiempo real.

Atributos de hilos.

   Los atributos o propiedades de un hilo varían de una implementación a otra. Sin embargo, de forma general los atributos que definen un thread son:
  • Estado de espera:  permite que otros hilos, esperen hasta que termine de ejecutarse un hilo en especial.
  • Dirección de stack. apuntador al inicio del stock del hilo.
  • Tamaño de la dirección: longitud del stock del hilo.
  • Alcance (scope): define quien controla la ejecución del hilo: el proceso o el núcleo del sistema operativo.
  • Herencia:  los parámetros de calendarización son heredados o definidos localmente.
  • Política de calendarización: se define que proceso se va a ejecutar y en qué instante.
  • Prioridad: un valor de prioridad alto corresponde a una mayor prioridad.
   Aunque el comportamiento en tiempo real, esta limitado a las capacidades del sistema operativo sobre el que corre, aún supera a los entornos de flujo único de programa (single-thread) tanto en facilidad de desarrollo, como en rendimiento.

   Mientras los procesos mantienen su propio espacio de direcciones y entorno de operaciones, los hilos dependen de un programa padre en lo que se refiere a recursos de ejecución.

   En Java, los hilos comparten el mismo espacio de memoria. Incluso comparten gran parte del entorno de ejecución, de modo que la creación de nuevos hilos es mucho más rápida que la creación de nuevos procesos. La ventaja que proporcionan los hilos, es la capacidad de tener más de un camino de ejecución en un mismo programa. 
Creación de hilos.

   En Java, existen dos mecanismo que nos permiten la creación de hilos:
  • Implementando la interfaz Runnable
  • Extendiendo la clase Thread, es decir, creando una subclase de ésta.  
   En cualquiera de los dos casos, se debe definir un método run que será el que incluya las instrucciones que se ejecutarán en el thread (hilo) y se pueden definir prioridades aunque no se puede confiar en que la máquina virtual escoja para ejecutar, siempre, el de mayor prioridad, por lo que no se pueden utilizar para basar en ellas el scheduler de un sistema en tiempo real.

   La clase Thread.

class Repeticion extends Thread {
          private int repeticiones;
          private String mensaje;
          Repeticion (String msg, int n) {
                mensaje = msg;
                repeticiones = n;
                }
         public void run() {
               for (int i= 1; i <= repeticiones; i++)
              System.out.println (mensaje + "  " +i);
              }
         public static vouid main (String args [ ] {
             Repeticion r1 = new Repeticion ("Rojo", 5);
             Repeticion r2 = new Repeticion ("Azul", 80);
             r1.start();
             r2.start();
             }
         }

    Cuando creamos un hilo extendiendo la clase Thread, se pueden heredar los métodos y variables de la clase padre. Si es así, una misma subclase solamente puede extender o drivar una vez la clase padre Thread. Esta limitación de Java puede ser superada a través de la implementación de Runnable que es una interfaz.

   La interfaz Runnable.

class Repeticion2 implements Runnable {
        private int repeticiones;
        private String mensaje;
        Repeticion2 (String msg, int n) {
              mensaje = msg;
              repeticiones = n;
              }
        public void run ( ) {
              for (int i=1; i<= repeticiones; i++;)
              System.out.println(mensaje  + "  " + i);
              }
         public static void main (String args [ ]) {
                  Repeticion r1 = new Repeticion ("Rojo", 5);
              Thread r2 = new Thread (new Repeticion2 ("Azul", 80))
               r1.start ();
               r2.start ();
               }
            }

 Arranque de hilos.
   Como se pudo apreciar en los ejemplos anteriores, el arranque de un hilo debe realizarse dentro del método principal de Java, que como todos sabemos, es el método main. Y lo arrancamos llamando al método start.

                  r1.start ( );

start, es el método oculto en el hilo cuya función es llamar al método run.

Manipulación de hilos.


   Una vez que realizamos la creación de un hilo, éste debe contener una traza de ejecución válida, la cual controlaremos en el método run del objeto.


   El cuerpo de ésta función (las acciones del hilo), vienen a ser el cuerpo del programa. Es como referirnos a la rutina main pero a nivel del hilo. Es decir, todas las acciones que nos interesa que nuestro hilo realice, deben estar especificadas en el método run. Al terminar de ejecutarse el método run, también terminará la ejecución de nuestros hilos.

   Por lo anterior, la manipulación de nuestro hilos, se realiza dentro del método run.

 Suspensión de hilos.

   También podemos realizar la suspensión de un hilo, es decir, detenerlo o desactivarlo  por un intervalo de tiempo indeterminado, para ésto utilizamos la función suspend.

   Este método no detiene la ejecución en forma permanente. El hilo es suspendido indefinidamente y para volver a activarlo nuevamente es necesario realizar una invocación a la función resume.

   Es importante mencionar, que también existe la función sleep, pero en ésta se especifica el tiempo en milisegundos en el que el hilo permanecerá "dormido" y al término de éste tiempo el hilo continua ejecutándose.

 Parada de hilos.


   El método que debemos utilizar para detener la ejecución de nuestro hilo, es stop, el cual detendrá la ejecución en forma permanente.

        t1.stop();


   Este método no destruye el hilo, simplemente detiene su ejecución y ésta no puede ser reanudada con el método start.


   Su utilidad tiene sentido, sobre todo, en aplicaciones complejas que necesiten un control sobre cada uno de los hilos que se ejecuten.

 Sincronización de hilos.

   La necesidad de la sincronización de hilos, tiene lugar cuando varios hilos intentan acceder al mismo recurso o dato. Es decir, los hilos necesitan establecer cierto orden, a la hora de acceder a datos comunes. Para asegurarse de que los hilos concurrentes no se estorban y operan correctamente con datos o recursos compartidos, un sistema estable previene la inacición y el punto muerto o interbloqueo. La inanición tiene lugar cuando uno o más hilos están bloqueados al intentar conseguir el acceso a un recurso compartido de ocurrencias limitadas.  El interbloqueo es la última fase de la inanición; ocurre cuando uno omás hilos están esperando una condición que no puede ser satisfecha. Esto ocurre muy frecuentemente cuando dos o más hilos están esperando a que el otro u otros desbloqueen algún dato u objeto común.

   Existen dos forma para aplicar la sincronización:
  • Bloqueo de objetos
  • Uso de señales.
  Uso de semáforos o señales.

   Dentro de éste sistema, un hilo puede detener su ejecución y esperar una señal de otro hilo para continuar con su ejecución.

   En este sistema encontramos varios sistemas como son el uso de mutex, semáforos y barreras.

Semáforos.

   En el caso de los semáforos, podemos establecer un número máximo de hilos que pueden tener acceso simultáneo a un recurso compartido en específico; es decir, es una variable especial que constituye el método clásico para restringir o permitir el acceso a recursos compartidos.

   Cada vez que un hilo intenta utilizar el recurso compartido, existe un contador que se va decrementando en uno y lo deja pasar. En el momento en que el contador se convierte en cero, deja bloqueado al hilo que intentó el acceso.

Inicia(Semáforo s, Entero v)
{
  s = v;       // Declara el contador de tipo entero
}
P(Semáforo s)
{
  if(s>0)      // Si aún no se ha excedido el número permitido deja pasar el hilo
      s = s-1; // Decrementa el contador 
  else         // Si ya se tiene el número permitido de hilos ejecutándose
      wait();  // Deja el hilo en espera
}
V(Semáforo s)
{
   if(!procesos_bloqueados)
        s = s+1;  
   else
        signal(); 
}
   Un tipo simple de semáforo es el binario, que puede tomar los valores de 0 y 1. Se inicializa en 1 y son usados
cuando solo un proceso puede acceder a un recurso a la vez.

Problemas al NO usar semáforos

Lo primero es ver una aplicación fictica corriendo SIN el uso de los semáforos, para luego entender mejor su utilidad. Su pongamos que tenemos 4 procesos (p1, p2, p3, p4), cada proceso realiza su “tarea” simultaneamente (durante un tiempo indefinido) y posteriormente termina. Supongamos además que necesitamos que se ejecuten primero los procesos P1 y P3, y luego P2 y P4. Tenemos entonces P1.java:
?
01
02
03
04
05
06
07
08
09
10
<span class="IL_AD" id="IL_AD8">public</span> class p1 extends Thread {
    public void run() {
        try {
            <span class="IL_AD" id="IL_AD10">sleep</span>((int) Math.round(500 * Math.random() - 0.5));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("P1");
    }
}
P2.java
?
01
02
03
04
05
06
07
08
09
10
public class p2 extends Thread {
    public void run() {
        try {
            sleep((int) Math.round(500 * Math.random() - 0.5));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("P2");
    }
}
P3.java
?
01
02
03
04
05
06
07
08
09
10
public class p3 extends Thread {
    public void run() {
        try {
            sleep((int) Math.round(500 * Math.random() - 0.5));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("P3");
    }
}
y P4.java
?
01
02
03
04
05
06
07
08
09
10
public class p4 extends Thread {
    public void run() {
        try {
            sleep((int) Math.round(500 * Math.random() - 0.5));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("P4");
    }
}
y la clase SinSemaforos.java, que lanza los subprocesos:
?
1
2
3
4
5
6
7
8
public class SinSemaforos {
    public static void main(String[] args) {
        (new Thread(new p1())).start();
        (new Thread(new p2())).start();
        (new Thread(new p3())).start();
        (new Thread(new p4())).start();
    }
}
Ejecutando varias veces este programa, tendremos salidas como:
# java SinSemaforos P4 P2 P1 P3 # java SinSemaforos P2 P1 P3 P4 # java SinSemaforos P2 P1 P4 P3
Como puedes ver, los procesos se ejecutan sin cumplir la condición más importante: que se ejecuten primero los procesos P1 y P3, y posteriormente P2 y P4. Solucionemos esto con el uso de semáforos!

Solución usando semáforos

En este caso vamos a usar la clase Semaphore, del paquete java.util.concurrent. A darle entonces: Tenemos entonces P1.java:
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
import java.util.concurrent.Semaphore;
public class p1 extends Thread  {
    protected Semaphore oFinP1;
    public p1(Semaphore oFinP1) {
        this.oFinP1 = oFinP1;
    }
    public void run()  {
        try {
            sleep((int) Math.round(500 * Math.random() - 0.5));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("P1");
        this.oFinP1.release(2);
    }
}
P2.java
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.concurrent.Semaphore;
public class p2 extends Thread  {
    protected Semaphore oFinP1;
    protected Semaphore oFinP3;
    public p2(Semaphore oFinP1,Semaphore oFinP3) {
        this.oFinP3 = oFinP3;
        this.oFinP1 = oFinP1;
    }
    public void run()  {
        try {
        this.oFinP1.<span class="IL_AD" id="IL_AD1">acquire</span>();
        this.oFinP3.acquire();
        } catch(Exception e) {
            e.printStackTrace();
        }
        try {
            sleep((int) Math.round(500 * Math.random() - 0.5));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("P2");
    }
}
P3.java
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
import java.util.concurrent.Semaphore;
public class p3 extends Thread  {
    protected Semaphore oFinP3;
    public p3(Semaphore oFinP3) {
        this.oFinP3 = oFinP3;
    }
    public void run()  {
        try {
            sleep((int) Math.round(500 * Math.random() - 0.5));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("P3");
        this.oFinP3.release(2);
    }
}
y P4.java
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.concurrent.Semaphore;
public class p4 extends Thread  {
    protected Semaphore oFinP1;
    protected Semaphore oFinP3;
    public p4(Semaphore oFinP1,Semaphore oFinP3) {
        this.oFinP3 = oFinP3;
        this.oFinP1 = oFinP1;
    }
    public void run()  {
        try {
            this.oFinP1.acquire();
            this.oFinP3.acquire();
            } catch(Exception e) {
                e.printStackTrace();
            }
        try {
            sleep((int) Math.round(500 * Math.random() - 0.5));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("P4");
    }
}
y la clase UsoSemaforos.java, que lanza los subprocesos:
?
01
02
03
04
05
06
07
08
09
10
11
12
import java.util.concurrent.Semaphore;
public class UsoSemaforos {
    protected static Semaphore oFinP1,oFinP3;
    public static void main(String[] args) {
         oFinP1 = new Semaphore(0,true);
         oFinP3 = new Semaphore(0,true);
         (new Thread(new p1(oFinP1))).start();
         (new Thread(new p2(oFinP1,oFinP3))).start();
         (new Thread(new p3(oFinP3))).start();
         (new Thread(new p4(oFinP1,oFinP3))).start();
    }
}
Ejecutando varias veces el programa, podemos ver como los subprocesos P1 y P3 se ejecutan siempre de primeras, y los procesos P2 y P4, de ultimas:
#java UsoSemaforos P3 P1 P2 P4 #java UsoSemaforos P1 P3 P2 P4 #java UsoSemaforos P3 P1 P4 P2 #java UsoSemaforos P1 P3 P4 P2

Conclusiones…

  • Lo primero es el método acquire() de la clase Semaphore. Este método bloquea el semáforo premanetemente (wait); mientras que,
  • el método release() de la clase Semaphore, libera el semáforo para los demás procesos (signal).















No hay comentarios:

Publicar un comentario