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 2

POOL DE THREADS


Un modelo hibrido toma aspectos comunes de cada enfoque

Para evitar este tipo de problemas la solución adoptada en la industria es el uso de brokers (distribuidores de un pool de recursos).-









En esta aproximación al problema, existe un administrador de los pedidos encolados, que se encarga de derivarla al primer thread libre.-


Por supuesto que definir la “habilidad” de este administrador para actuar como un broker no es sencillo.-

Un buen administrador del pool, no debe limitarse a habilitar N threads, ya que quizás N sea un numero conservador (y por ende el procesador será subutilizado) o N es un numero ambicioso y podemos llegar al problema de aumento geométrico de la carga descrito al inicio de este capitulo.-

El broker debe descubrir EN TIEMPO REAL, si existe o no potencia de procesamiento ociosa para decidir si habilita un nuevo thread o espera que alguno se libere.-

La solución en C# con pool de threads



Siendo este un problema común, dentro del Framework de .NET tenemos un namespace que nos brinda un administrador de un pool de threads.-
El namespace se denomina System.Threading y la clase se llama ThreadPool.-
Dentro de esta clase tenemos todos métodos estáticos, lo que permite utilizarlos con llamadas globales.-
Veamos un programa que presenta una aproximación al problema con las herramientas que nos brinda C#.-

using System;



using System.Threading;


//En este ejemplo utilizamos una técnica de threading en la que enviamos
// como parámetro del constructor (WaitCallback) un METODO de la clase
// es decir a diferencia de como se puede implementar este tipo de técnicas en Java
// (que se pasan objetos) acá mandamos el nombre del método (función) que se debe ejecutar


namespace ThreadPoolTest


{


class MainApp


{


static void Main()


{


WaitCallback callBack;


callBack = new WaitCallback(FuncionAEjecutar);


ThreadPool.QueueUserWorkItem(callBack,"Hilo 1");


ThreadPool.QueueUserWorkItem(callBack,"Hilo 2");


ThreadPool.QueueUserWorkItem(callBack,"Hilo 3");


Console.ReadLine();


}


static void FuncionAEjecutar(object state)
{
    Console.WriteLine("Procesando requerimiento '{0}'." +


          " El thread esta dentro del pool ?: {1}, Hash: {2}",


                (string)state, Thread.CurrentThread.IsThreadPoolThread,


       Thread.CurrentThread.GetHashCode());

      // Simulacion de tiempo de procesamiento


      Thread.Sleep(2000);


      Console.WriteLine("Requerimiento '{0}' procesado",


                        (string)state);
}


}


}











Este es un ejemplo interesante que nos muestra cuando puede ser útil en un proceso batch el uso de hilos múltiples.-


Observe que cada llamada a la función se ejecuto en un thread independiente, mientras en otro thread se ejecutaba otra llamada a la función.-

Esto puede parecerse a un programa de comunicaciones que puede atender llamadas multiples.-

Frente a cada llamada levanta un thread, pero cada thread llama a la misma funcion de atencion.-

Lo que hicimos en la función llamada FuncionAEjecutar() fue hacerle perder tiempo (Thread.Sleep) para poder ver en la consola esta “simultaneidad” de procesamiento.-

Observe, como comentamos líneas arriba, que los implementadores de la clase han decidido que todos los métodos sean públicos y estáticos.-

El objetivo es hacer de esto un Singleton, es decir que solo exista una instancia de la clase para todo el proceso o sesión.-

Si bien esto podría haberse hecho con un enfoque mas orientado a objetos (constructor privado, interfaz para implementar las funciones a llamar, polimorfismo para las llamadas de las funciones a colocar en el thread), los implementadores entendieron que no era el mejor camino, al menos en C#.-

Esto nos habla de decisiones de diseño, algo que todos los ingenieros de software deben (o deberían) aprender.-

Hagamos una pequeña variante al ejemplo anterior para ver más claramente la ejecución en hilos separados.

En este caso la función llamada en el primer thread termina antes que el disparo de todos los threads requeridos.-

Pero esto no afecta para nada el enfoque del programa.-

using System;


using System.Threading;


//En este ejemplo utilizamos una tecnica de threading en la que enviamos
// como parametro del constructor (WaitCallback) un METODO de la clase
// es decir a diferencia de como se puede implementar este tipo de tecnicas en Java
// (que se pasan objetos) aca mandamos el nombre del metodo (funcion) que se debe ejecutar


namespace ThreadPoolTest
{


class MainApp
{
static void Main()
{

WaitCallback callBack;
callBack = new WaitCallback(FuncionAEjecutar);
string s;


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


       s = "Hilo nro: " + i;
      ThreadPool.QueueUserWorkItem(callBack,s);
}


Console.ReadLine();


}


static void FuncionAEjecutar(object state)
{


              Console.WriteLine("Procesando requerimiento '{0}'." + " Hash: {1}",string)state,Thread.CurrentThread.GetHashCode());


               // Simulacion de tiempo de procesamiento


               Thread.Sleep(2000);
               Console.WriteLine("Requerimiento '{0}' procesado", (string)state);
}


}


}














Entendiendo mejor el problema



Ahora bien, el recurso que todos queremos utilizar es el procesador.-


¿Que pasara si la función que se coloca en el thread consume recursos?


Pensemos que nosotros adrede hicimos que nuestra función NO CONSUMIESE RECURSOS de CPU (ese es el sentido de Sleep ¡!) sino que perdiese tiempo.-


Lo que haremos ahora es que nuestra función, en esos 2 segundos (o 2000 milisegundos que es como se lo indicamos a Sleep) trabaje compulsivamente.-


Para eso lo haremos iterar durante esos 2 segundos:






int ticks = Environment.TickCount;


//Trabajar mientras no hayan transcurrido los 2 segundos


while(Environment.TickCount - ticks < 2000);






Es decir la versión de programa ahora presentara un cambio en la función llamada:


using System;


using System.Threading;
namespace ThreadPoolTest


{


 class MainApp


 {
  static void Main()
  {


          WaitCallback callBack;
          callBack = new WaitCallback(FuncionAEjecutar);
          string s;


          for (int i=0; i < 8; i++)
        {
             s = "Hilo nro: " + i;
            ThreadPool.QueueUserWorkItem(callBack,s);
        }


       Console.ReadLine();


}


static void FuncionAEjecutar(object state)
{


    Console.WriteLine("Procesando requerimiento '{0}'." + " Hash: {1}",
     (string)state,Thread.CurrentThread.GetHashCode());
   
     // Simulacion de tiempo de procesamiento
      int ticks = Environment.TickCount;


     //Trabajar mientras no hayan transcurrido los 2 segundos
      while(Environment.TickCount - ticks  <  2000);
        
          Console.WriteLine("Requerimiento '{0}' procesado",(string)state);


}


}


}

Observe como el procesador inmediatamente queda sobrecargado (el efecto del while es mantenerlo “entretenido”), y la cantidad de threads se mantiene baja.


Esto es porque a partir del 3er pedido, (hilo 2), los mismos no se procesan hasta que no termine el anterior y libere un thread.-

Esa es la acción inteligente, que esperábamos de un broker que administre el pool de threads.-



 
 
 
 
 
 
 
 
 
 
 
 
Los timers como herramientas en hilos múltiples



En nuestros manuales Curso MFC-II.doc y Curso MFC-III.doc hablamos de los timers de Win 32, utilizados dentro de aplicaciones C++.-
La deficiencia del uso de un CONTROL timer para el control de tiempo, es que se debe reposar en técnicas no seguras para la ejecución del tick del reloj.-
Esto es porque los eventos levantados por este control (o API o clase dependiendo de cómo lo utilice) son sincrónicos respecto de la aplicación que lo ha instanciado.-

Es decir, se debe esperar que la aplicación Windows (es decir hace falta una aplicación con ventanas) pase el control a la ronda de atención de mensajes de Windows y allí sea atendido por el mensaje WM_TIMER.-
Esto significa que si la aplicación esta ocupada en otras tareas y su tiempo de ronda de atención, supera al del evento Elapsed(periodo a controlar) del control Timer, este perderá 1 o mas rondas de ejecución.-


En el Framework .NET existen 3 tipos de Timers.-

Server Based Timers

Uno es el que se ve en la solapa Componentes del Toolbox.-




















Este timer es llamado timer basado en servidor.-


Puede trabajar indistintamente con Windows Forms o con servicios batch (sin interacción con el usuario) y se encuentra en la biblioteca de clases llamada System.Timers.Timer

Esta diseñado para trabajar con hilos multiples y en los hilos llamados “de trabajo” (worker threads) que son aquellos que no atienden servicios de interfaz con el usuario.-

Su arquitectura difiere de la anterior, al punto que son más exactos que los Windows timers.-

Pero la diferencia esencial es que el mismo timer puede manejar un evento levantado en OTRO THREAD.-



Windows Timer

El segundo es el control de tiempo basado en ventanas de Windows y es una actualizacion del que existe desde las primeras versiones de Visual Basic.

Es una versión idéntica a la de los controles timer de WIN 32 y esta pensada solo para trabajar con Windows.Forms, y se encuentra en la biblioteca de clases llamada System.Windows.Forms.Timer.-

Este timer (Windows Timer) esta diseñado para ambientes de hilo unico (single threaded) que es el unico en el que puede trabajar Visual Basic 6.0 y anteriores.-



Thread Timers


EL tercero es un timer para trabajo con hilos. Es el que utilizaremos en nuestros ejemplos y utiliza metodos de llamadas a funciones como parametros (callbacks).-
Este timer no tiene un control accesible como componente C# o de un Form y solo puede especificarse programaticamente.-
Las versiones del timer para trabajos con hilos, utilizaran las utilidades que figuran en el namespace System en System.Threading.Timer.-
El siguiente ejemplo al que llamaremos Timers_1 nos muestra algunos aspectos interesantes del uso de los timers de la biblioteca mencionada.-
El main instancia un objeto de la clase que crea los timers:

PruebaTimer test = new PruebaTimer();

Envia un mensaje por la consola:

Console.WriteLine("Timer iniciado por 6 segundos.");

y luego permite la ejecucion de los threads levantados en el constructor de la clase PruebaTimer
 
using System.Threading;

namespace Timers_1


{
  public class PruebaTimer
  {


     public ManualResetEvent timerevent;
     public PruebaTimer()
    {
            timerevent = new ManualResetEvent(false);
            //El primer timer trabaja CADA 5 segundos y en cada lapso
           //invoca al metodo Metodotimer1
         
        Timer timer = new Timer(new TimerCallback(this.MetodoTimer1),
                         null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));






//El segundo timer trabaja CADA 1 segundo y en cada lapso
//invoca al metodo Metodotimer2


Timer Timer2 = new Timer(new TimerCallback(this.MetodoTimer2),
                                               null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));






   }






   public void MetodoTimer1(object state)
 {


        Console.WriteLine("\nEn el 6to segundo,el Timer invoca este metodo.");


          timerevent.Set();


  }






public void MetodoTimer2(object state)
{


     Console.Write(".");


}


public static void Main()
{


   PruebaTimer test = new PruebaTimer();


   Console.WriteLine("Timer iniciado por 6 segundos.");


   //Bloquea el thread principal hasta que reciba la señal
  //del thread hijo (en este caso el creado con PruebaTimer)


    test.timerevent.WaitOne();


    Console.WriteLine("Mientras no Pulse ENTER para terminar, el timer seguira ejecutandose");


    Console.ReadLine();


}


}


}





Observemos lo que nos informa la ejecucion del programa en su salida por la consola:


A) Primero se ejecuta la sentencia del main, pese a haber instanciado previsamente un objeto de la clase Timer. Esto sucedera hasta que el thread del main quede bloqueado y eso permita a los otros threads seguir ejecutandose.-
El bloqueo del thread del main se produce al ser invocado el metodo WaitOne del atributo publico timerEvent de la clase PruebaTimer.-



test.timerevent.WaitOne();





B) Luego se ejecutan los metodos llamados por los timers.-

Se observa que lo hacen en estricto orden de creacion .Es decir primero el metodo llamado el primer timer instanciado y luego el metodo del segundo timer instanciado.-



Timer timer = new Timer(new TimerCallback(this.MetodoTimer1),


…..



Timer Timer2 = new Timer(new TimerCallback(this.MetodoTimer2),

….


C) Finalmente observemos que en la instanciacion del timer, se coloca no solo el metodo que sera invocado en cada lapso, sino ademas cada cuanto tiempo, dicho lapso se ejecuta.-

//El primer timer trabaja CADA 5 segundos y en cada lapso
//invoca al metodo Metodotimer1


Timer timer = new Timer(new TimerCallback(this.MetodoTimer1),
                                                    null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));



D) El modo en el que los threads se informan del inicio y fin de su actrividad, permite una sincronizacion simple.

En el ejemplo elegimos usar la clase ManualResetEvent que:

1- Nos permite a traves de WaitOne detener un thread bloquendo su ejecucion y permitiendo que otros threads encolados puedan ejecutarse

2- Y a traves de Set nos permiter levantar una señal indicando la finalizacion de la tarea de un thread para que el que estaba bloqueado continue.-

E) La ejecucion nos muestra que el timer, una vez que está levantado, se ejecuta con periodicidad, pese a que el thread principal es el que tiene nuevamente el control.-

Se puede observar que los metodos llamados en cada lapso (eventos) son ejecutados hasta que se pulsa la tecla ENTER.-

No hay comentarios: