Cualquier cosa que valga la pena se hace en equipo

Cualquier cosa que valga la pena se hace en equipo

sábado, 24 de abril de 2010

Algo sobre Threads en C#-Nota 1

OBJETIVO

Revisar algunos conceptos sobre hilos en C#(multiple threading).-

Ejecución de Hilos Múltiples en un proceso


El uso de hilos de ejecución dentro de un proceso presenta esencialmente ventajas en los programas con interacción con el usuario.-
El punto básico es que mientras se ejecuta una tarea, el programa comienza a atender otra en paralelo.-
Esto hace que quien interactúa con el programa no quede a la espera de la ejecución de una tarea que puede ejecutarse en trasfondo (background), mientras continua con su trabajo.-

Bien, este es el marco general que podemos extender no solo a aplicaciones interactivas sino también a procesos batch.-

Ahora bien, ¿es todo TAN SIMPLE como “simular” que tenemos mas de un procesador atendiendo una tarea?

¿Se trata solo de abrir otro hilo de ejecución y “mágicamente” toda la operación se acelera como si tuviese dos procesadores atendiendo?

Veamos un poco más en detalle el tema con el esquema siguiente.-


Imaginemos un proceso que actúa como un servidor de pedidos y que se ejecuta en un solo hilo de ejecución.-


Observamos, y esperamos, que cada pedido se encola, el primero que ingresa es el primero que se atiende y solo se pasa al siguiente cuando se finaliza con el anterior.-
Ahora bien, un programa como el que mostramos difícilmente utilice un solo recurso de la computadora.-
¿Que queremos decir con esto?
Que si por ejemplo el programa esta atendiendo el pedido 1 (Hilo 1) y, dentro de sus tareas esta una pesada consulta a la base de datos, el pedido quedara detenido hasta que la consulta haya sido satisfecha (por otro recurso u otro proceso), pero mientras tanto mantiene encolados a los demás pedidos, AUN CUANDO NO UTILICE COMPLETAMENTE, POR EJEMPLO, EL PROCESADOR.-

Esta idea se extiende a cualquier otro recurso (discos, impresoras, lectores ópticos, canales de entrada de información, etc.) que un proceso deba utilizar.-

Surge entonces la idea de: ¿por que no usar los recursos que este hilo no utiliza en este momento?




 En esta técnica hemos GENERADO un nuevo thread por cada pedido.-


Debemos ser cuidadosos en este tipo de decisiones.-
Si el proceso que tenemos en nuestro programa, es simple,entonces un aumento en el numero de llamadas a la función a través de nuevos threads, puede generar una sobrecarga inesperada: Que el cambio de contexto (intercambio entre los threads) demore mas tiempo que la función que se ejecuta en el thread.-
Con esto tenemos uno de los peores escenarios, ya que a medida que se aumenta el uso del sistema (escalabilidad), el tiempo de espera comienza a aumentar lineal o geométricamente impidiendo que el sistema pueda escalarse de modo armonioso.-

Esto es así ya que el Thread es una “simulación” de procesamiento múltiple paralelo. La verdadera ejecución en paralelo solo puede hacer con procesadores dedicados a cada hilo del proceso.-
Aun en el caso de tener más de un procesador podemos llegar al escenario que se plantea en el párrafo anterior. Aunque por supuesto el sistema en su conjunto admitiría una carga mayor antes de llegar a esa situación -

Un modelo con multiples threads


Los hilos en Windows tienen el siguiente esquema:
Una aplicación comienza con un thread (hilo) específico: que es su hilo de ejecución.-
Esto le asigna (al proceso) memoria, espacio en la pila y desde ya tiempo de procesador.-

Puede luego expandir otros hilos.-
Windows tiene un planificador de tareas que divide el tiempo de CPU entre los threads activos.
Toda la planificación se hace estrictamente por prioridad. El planificador elige el hilo de mayor prioridad y lo ejecuta.-

Si la computadora tiene un solo procesador, este tiempo se reparte entre ellos. Unos párrafos mas adelante explicamos de un modo bastante general como se realiza esta tarea.-
Si la computadora tiene mas de un procesador, cada uno de ellos atenderá un thread por ronda del scheduler. Es decir que si hubiese N procesadores (en un sistema multiprocesador) los N primeros threads ejecutables serán ejecutados.

Prioridades


La prioridad utilizada para hacer estas decisiones es la prioridad dinámica del thread.-
Cada proceso ejecutable tiene una base de prioridad para ser ejecutado. Cada thread tiene también una base de prioridad que es función de la base de prioridad del proceso que la dispara. Decimos que es función de el porque es ajustable en:



- 1 o 2 puntos por sobre su proceso base

- igual a su proceso base

- 1 o 2 puntos por debajo de su proceso base.-



Este ajuste de prioridad esta expuesto a través de las API de Win32.-
Además de la prioridad base, todos los threads tienen una prioridad dinámica que NUNCA es menor QUE LA PRIORIDAD BASE. El sistema aumenta y decrementa esta prioridad base según sus necesidades.-
Esto lo hace para resolver un interesante caso de multiprocesamiento:
Cuando el kernel del sistema operativo esta eligiendo cual es el thread que va a ejecutar en el procesador, elige aquel que tenga la variable dinámica más alta (esto es el de prioridad dinámica mas alta).-
Pensemos un caso donde 3 hilos: T1, T2 y T3 con prioridades crecientes.-
T1, el de mayor prioridad esta listo para ser programado, mientras que T3 (el de menor prioridad) se esta ejecutando en una sección critica.(Mas adelante explicamos que son las secciones criticas)-
Ahora T1 queda esperando que T3 libere un recurso compartido (cualquiera).-
Entonces T2 toma todo el tiempo del procesador ya que T1 esta ocupado esperando el recurso que debe liberar T3.-
Como T3 no es de alta prioridad en este esquema no es atendido.

Lo que significa que no puede salir de la sección critica ya que no puede ser programado por el scheduler.-
AL no salir de esa sección, no libera el recurso compartido y T1 no puede seguir ejecutándose.-
Para resolver esto, el planificador (scheduler) de Windows resuelve esto invirtiendo de modo aleatorio las prioridades de los threads que están listos para ser ejecutados. De este modo se deja al thread T3 ejecutarse el tiempo suficiente para liberar el recurso que estará esperando T1.-


Si el hilo de baja prioridad (T3) no tuvo tiempo suficiente de liberar su bloqueo, se le dará otra oportunidad en la próxima ronda.-



Tiempo de atención

Cuando el thread esta planificado para correr en el procesador tiene asignada una cantidad (quantum en la jerga técnica) de tiempo para ejecutarse. Realmente se le entrega a cada uno 2 unidades de quantum (algo así como 10ms en r4000 y 15ms en x86).-
El quantum (tiempo de atención) de un thread, resulta decrementado cuando en una interrupción del reloj del procesador, se descubre que esta corriendo. En ese momento su quantum se decrementa en 1.-
Cuando el quantum de un thread llega a cero, su prioridad dinámica se decrementa en 1, SOLO en el caso en que esta no coincida con la prioridad base. En ese caso también el quantum del thread es reasignado al valor de la prioridad base. Todo este proceso es para intentar asegurar un balance en los tiempos de atención-
Si ocurre un cambio de prioridad, entonces el scheduler ubica el thread de máxima prioridad que esta listo para correr.

Sino el thread se coloca al final de la cola de ejecución de su prioridad permitiendo que otros threads de la misma prioridad se planifiquen para ser corridos en un proceso conocido como “round robin”.-
En C#, la clase Thread permite que generemos hilos múltiples con responsabilidad de nuestro programa de no generar una situación como la indicada en el párrafos anteriores.-

Una vez creado el thread se utilizara ThreadStart para indicar cual es el código del programa que se ejecuta en el thread.
Durante la duración de su existencia, un thread esta en uno o mas de los estados definidos en ThreadState.

La calificación de nivel de prioridad del scheduler se define en ThreadPriority, pero no hay garantía que el sistema operativo pueda respetarla.-
GetHashCode provee un mecanismo simple de identificación de los threads en ejecución.-
Durante la vida del thread en el proceso este identificador es exclusivo del thread. El valor de GetHashCode no tiene relación con el ThreadId que el Sistema operativo asigna al mismo.-
Veamos un ejemplo que nos permitirá ensayar algunas variantes y entender como funcionan los threads.-
En este programa el thread principal (el del método Main) arranca 2 nuevos threads.-



Thread t1 = new Thread(new ThreadStart(ProcesoDelThread1));






Thread t2 = new Thread(new ThreadStart(ProcesoDelThread2));



Cada thread responderá a la ejecución de 2 métodos (ProcesoDelThread1 y ProcesoDelThread2).-
Hasta allí el esquema es simple.-

Pero hemos hecho una diferencia operativa entre ambas funciones.-
El método llamado por el primer Thread (ProcesoDelThread1) consume tiempo de procesador en el momento en el que le toca trabajar, mientras que el método llamado por el segundo Thread (ProcesoDelThread2) es mas “solidario”. Comparte su tiempo de ejecución con los otros threads del mismo proceso.-


Es decir en el primer thread (t1 -> ProcesoDelThread1) el código procura ejecutar su tarea sin compartir su tiempo de ejecución con otros procesos.-

El while se encarga de eso:


 int c = 0;
//Consumir procesador
while(c < T_MAX)

   c++;
Pero en el segundo thread (t2 -> ProcesoDelThread2) el codigo esta escrito permitiendo compartir el uso del procesador durante la ejecución DEL MISMO THREAD (es decir comparte sin esperar a terminar).-

El metodo Sleep es el que se encarga de esto

Thread.Sleep(0);

Para observar el mismo efecto en el thread principal, hemos agregado una condición dentro de la iteración principal:


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


    Console.WriteLine("Thread Principal: Haciendo algo en la vuelta {0}.", i);
    if (i> 4)
        //Dejar que otros se ejecuten
          Thread.Sleep(0);
}


Es decir a partir de ejecutada la 6ta (las vueltas 0 a 4 no cumplen la condición y la 5ta la cumple, pero ejecuta parte de su tarea antes de compartir tiempo de procesador).-




Ejecute el código que figura a continuación como una aplicación de consola y compruebe la salida.-



using System;


using System.Threading;
//

// Un escenario de thread simple :comenzar metodos estaticos corriendo
// en otros dos threads
// En este ejemplo cuando existe un unico procesor en la computadora en la que se ejecuta
// el thread no utiliza tiempo del procesador, hasta que el proceso principal se lo permite
// Esto se logra cuando utilizamos el metodo estatico sleep de la clase Thread.-
//


public class ThreadExample
{


           const int T_MAX = 200;


// Esta funcion llamada por el thread1 no permite que otros threads se ejecuten
//Hasta que el termine


public static void ProcesoDelThread1()
{


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


{


             Console.WriteLine("Proceso Hijo llamado ProcesoDelThread1: {0}", i);


//
              int c= 0;
                 //Consumir procesador


              while (c < T_MAX) c++;
}


}

//Esta funcion llamada por el thread2 deja en cada iteracion un tiempo para

//la ejecucion de otros threads

public static void ProcesoDelThread2()


{


for (int i = 0; i < 12; i++)


{


         Console.WriteLine("Proceso Hijo llamado ProcesoDelThread2: {0}", i);


           // Indicar que el thread debe suspenderse para permitir que otros
           // se ejecuten


           Thread.Sleep(0);


           //Realizar el trabajo restante


          Console.WriteLine("Segunda parte de ProcesoDelThread2: {0}", i);


}


}

public static void Main()



{


Thread t1 = new Thread(new ThreadStart(ProcesoDelThread1));
Thread t2 = new Thread(new ThreadStart(ProcesoDelThread2));


//Iniciar los threads


Console.WriteLine("Thread Principal: Inicia thread hijo t2");


t2.Start();


Console.WriteLine("Thread Principal: Inicia thread hijo t1");


t1.Start();


for (int i = 0; i < 8; i++)


{


         Console.WriteLine("Thread Principal: Haciendo algo en la vuelta {0}.", i);


          if (i> 4)
           //Dejar que otros se ejecuten
               Thread.Sleep(0);


}


Console.WriteLine("Thread Principal: llamo al metodo Join(), para esperar" +


" que la funcion del primer thread termine");


t2.Join();


Console.WriteLine("Thread Principal: Retorno de la ejecucion"+


" del segundo thread. Pulse ENTER para terminar");


Console.ReadLine();


}


}



Veamos esto por partes para tener un mejor entendimiento

Las primeras líneas de salida corresponden a la ejecución del thread principal y de su ciclo for, hasta la vuelta numero 6 (es decir i == 5).-


En esa vuelta se cumple la condición que permitirá compartir el tiempo de procesamiento
Allí se da tiempo para ejecutar el trabajo de t2.-

Lo que efectivamente sucede, PERO OBSERVE QUE INMEDIATAMENTE se permite que el thread principal continúe (“Thread principal haciendo algo en la vuelta 6”)-
Como el thread principal es también cortés, vera que luego de esto el procesador vuelve a atender al thread t2 y allí este termina su primer vuelta realizando la segunda parte de su trabajo (“Segunda parte de ProcesoDelThread2: 0”).-

Pero a partir de alli, la cortesía del thread principal y de t2, no tiene nada que hacer con la “avaricia” de t1.-
Este arranca y hasta que no termina su tarea no permitirá que otro thread realice su trabajo.-



En esa vuelta se cumple la condición que permitirá compartir el tiempo de procesamiento



Allí se da tiempo para ejecutar el trabajo de t2.-





Lo que efectivamente sucede, PERO OBSERVE QUE INMEDIATAMENTE se permite que el thread principal continúe (“Thread principal haciendo algo en la vuelta 6”)-



Como el thread principal es también cortés, vera que luego de esto el procesador vuelve a atender al thread t2 y allí este termina su primer vuelta realizando la segunda parte de su trabajo (“Segunda parte de ProcesoDelThread2: 0”).-

Pero a partir de alli, la cortesía del thread principal y de t2, no tiene nada que hacer con la “avaricia” de t1.-
Este arranca y hasta que no termina su tarea no permitirá que otro thread realice su trabajo.-
 
 
Ahora bien, no parece cómodo tener que colocar un Thread.Sleep(0), por ahí, a los efectos de simular multiprocesamiento.-

Esto puede hacerse en ciertos métodos pero quizás no es útil en la mayoría

(Sigue en Algo sobre Threads en C#-Nota 2)

1 comentario:

Anónimo dijo...

Muchas gracias por la información. No es fácil encontrar algo así y bien explicado.