Instituto Tecnológico de Iztapalapa.

Bienvenidos!! espero esta información les sea de gran utilidad.... así que, comencemos!

Unidad 3. Programación concurrente multihilo.

  Al escuchar la palabra multi-hilo, tal vez lo primero que te viene a la mente son muchos "hilos" de los que conocemos normalmente en casa, pero al hablar en términos de programación, no nos estamos refiriendo a esos "hilos".
  
   En programación, nos estamos refiriendo a los lenguajes de programación que permiten la ejecución de varias tareas en forma simultánea.

   Por ejemplo, consideremos la cantidad de aplicaciones que corren a la vez dentro de un mismo entrono gráfico. Mientras una persona escribe un documento, esta corriendo Microsoft Windows además de Internet Explorer, Windows Explorer, CD Player y el Control de Volúmen. Estas aplicaciones son ejecutadas dentro de alguna versión de Windows. De esta forma, podemos pensar que los procesos son análogos a las aplicaciones o a programas aislados, pero realmente tiene asignado espacio propio de ejecución dentro del sistema.

   Java, es un lenguaje multihilo, ya que permite la ejecución de varias actividades en forma simultánea, tanto en un programa creado en este lenguaje como en el corazón mismo del lenguaje (en la parte interna). Teniendo como resultado, que se pueden usar hilos Java como standard, en cualquier plataforma.

3.1 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. 

 3.2 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.

   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.
3.3 Creación y control de hilos.
3.3.1. 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.
 3.3.2. 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 ();
               }
            }

3.3.3. 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.

3.3.4. 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.

3.3.5. 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.

3.3.6. 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.

3.4. 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.
   Bloqueo de objetos.

   Un objeto es bloqueado para indicarle a los demás hilos que estan en ejecución y que pudieran intentar acceder a éste que ya un hilo lo esta utilizando, para esto utilizamos la palabra synchronized en la definición de los métodos que tienen la posibilidad de tener éste problema de sincronización, por ejemplo:

          public synchronized int getNumero();

   De ésta forma, cuando un objeto este ejecutando el método getNumero que utiliza synchronized, se establece un bloqueo en dicho objeto para que ningún otro hilo pueda utilizarlo hasta que termine de ser usado por el hilo actual.

   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.

3.4.1. Mutex.

   También conocida como cerraduras de exclusión mutua, se utilizan para implementar secciones críticas y proteger las estructuras de datos compartidas contra accesos concurrentes.

   Se dice que las variables mutex, es la forma más fácil de implementar la sincronización de hilos y de proteger recursos compartidos cuando acontecen multitud de intentos de acceso sobre esos recursos. Dicha "protección" la realizan al "cerrar el candado o cerradura" para no permitir el acceso.

   Solo un hilo puede cerrar un candado en un determinado instante. Incluso si varios hilos intentan cerrar el mismo candado solo uno saldrá victorio. Ningún otro hilo podrá poseer dicho recurso hasta que el hilo que lo cerró lo abra.

3.4.2. 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. Son esencialmente lo mismo que el mutex.
 
3.4.3. Barreras (Barrier).
Las barreras son otro método de sincronización, y el más fácil de entender. Una barrera es un punto de encuentro
entre varios hilos, en donde todos los hilos ejecutan una acción o diferentes acciones hasta llegar al barrier o punto
de encuentro, una vez que han llegado todos los hilos, éstos pueden continuar con su ejecución. Esto se utiliza
generlamente, cuando una aplicación funciona por fases.
La clave en éste método de sincronización es el método wait() que hará que los hilos esperen hasta la llegada de
todos.