Cualquier cosa que valga la pena se hace en equipo

Cualquier cosa que valga la pena se hace en equipo

lunes, 26 de abril de 2010

Algo sobre threads en C# - Parte III



OBJETIVO


Veamos ahora un modelo de implementación de threads así como la implementación de dos patrones de desarrollo clásicos: Command y Strategy.-

Para eso vamos a plantearnos un motor batch , es decir un proceso que sea capaz de disparar procesos batch (no interqactivos ) en diferentes hilos de ejecucion


Un motor de procesos batch

Un motor de procesos batch es un software que es capaz de ejecutar procesos batch de modo concurrente y controlado.-

Lo que expondremos acá es un modelo simple del mismo del que después pueda derivarse un motor de uso profesional.-

Lo importante del motor desde el punto de vista de la arquitectura es que se trata de una aplicación que es capaz de manejar múltiples hilos de ejecución, colocar en cada uno de esos hilos un proceso a ejecutar , lanzar dichos procesos , esperar su conclusión y reportar que el proceso finalizo.-


Lo importante del motor desde el punto de vista de la técnica de diseño es que cumple con un patrón de los enunciados por Gamma y otros (GoF [1]).
Este patrón se denomina Command (Comando).

La idea de Comando como patrón de nuestro diseño es un objeto que tenga 3 estadios clásicos: Inicio, ejecución y cierre. Llamaremos a esos métodos como Inicio(), run() y CleanUp().-

Caulquier implementacion que tengo estas 3 tareas sera apra nostros un comando.
Pensemos entonces en una estrcutura de programacion que nos permita sugerir esta plantilla antes de implementarla. Llamamos a esto Interfaz

La Interfaz Comando




using System;


using System.Collections.Generic;
using System.Text;
namespace ThreadPoolTest_5


{


public interface Comando


{


void inicio();


void run();


void cleanup();


}


}

Las clases que implementan la interfaz Comando


Veamos ahora clases concretas de la Interfaz comando. Estas clases implementan los métodos declarados en la interfaz.-

Daremos dos implementaciones una llamada miClase y la otra llamada miOtraClase.-

No tienen nada de particular salvo que ambas implementan a la interfaz Comando.-

La clase miClase

Esta primera funcion solo consumo tiempo de procesamiento

using System;


using System.Collections.Generic;


using System.Text;


using System.Threading;






namespace ThreadPoolTest_5


{


public class miClase : Comando


{


private string _nombre;


private bool _terminado;


public string nombre
{
get { return _nombre; }
set { _nombre = value; }
}





public bool terminado
{
get { return _terminado; }
set { _terminado = value; }
}






public void inicio()


{


_terminado = false;


}





public void run()
{


Console.WriteLine(this.GetType().ToString() + " :Procesando requerimiento '{0}'." +


" Hash: {1}", (string)nombre, 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)nombre);


}


public void cleanup()
{


_terminado = true;


}


}// de clase


}// de namespace




La clase mi otraclase

Esta segunda funcion calcula el coseno de un angulo en radianes por el metodo de expansion:

using System;


using System.Collections.Generic;


using System.Text;


using System.Threading;





namespace ThreadPoolTest_5
{
public class miOtraClase : Comando
{


private string _nombre;
private bool _terminado;


public string nombre
{
get { return _nombre; }
set { _nombre = value; }
}




public bool terminado
{
get { return _terminado; }
set { _terminado = value; }
}


public void inicio()
{
_terminado = false;
}


public void run()
{


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




//Calcular
CalcularCosenos();


Console.WriteLine("Requerimiento '{0}' procesado", (string)nombre);


}


private void CalcularCosenos()
{
const double EPSILON = 0.000000001;
const double DESDE = 0.5346222;
const double HASTA = 3.1415926;
const double PASO = 0.000001999;


double lRadianes = DESDE;


double RadCuadrado;


double s;


double t;


double dFabsLRas;


double dFabsProd;


int k;


string Aux = "";




while (lRadianes < HASTA)
{
k = 0;
t = 1;
s = 1;
RadCuadrado = lRadianes * lRadianes;
dFabsLRas = Math.Abs(t);
dFabsProd = (EPSILON * Math.Abs(s));

//Calculamos la expansion hasta que el termino enesimo no sea mayor a un EPSILON DADO
while (dFabsLRas > dFabsProd)
{


k = k + 2;


t = (-1) * t * s * RadCuadrado/ (k * (k - 1));


// 1 - x^2/2! + x^4/4! - x^8/8! ...


s = s + t;


dFabsProd = (EPSILON * Math.Abs(s));


dFabsLRas = Math.Abs(t);


}


Aux = lRadianes.ToString() + ":" + s.ToString();
Console.WriteLine(Aux);
lRadianes += PASO;


}

}


public void cleanup()
{


_terminado = true;


}


}


}


El motor batch de tareas asíncronas


Ahora vamos a implementar el motor.-

El motor de tareas asíncronas representa una implementación de un modelo de procesamiento de un software capaz de disparar OTRAS tareas según una configuración de tiempos.-

En el ejemplo estamos utilizando 4 conceptos sobre los que el motor esta diseñado:

1- Para que el motor deba ejecutar un proceso en hilos (threads) múltiples usaremos un namespace llamado System.Threading en el que encontramos una clase llamada WaitCallback que es la implementación de un prototipo de función CallBack [2].-

2- La clase WaitCallback, necesita en su constructor un método que cumpla con este prototipo :

private static void MiFuncion(object miObjeto);

De modo que nosotros creamos un método dentro de nuestra clase de implementación del motor que pueda cumplir con este prototipo.-

3- La clase ThreadPool, con métodos estáticos públicos es la que nos permitirá levantar cada uno de los procesos que el motor batch dispara. ThreadPool nos permite definir la cantidad de threads que se abrirán en la sesión así como encolar los procesos que disparamos de modo de ejecutarlos secuencialmente.-

4- EL motor tiene un método llamado Trabaja() que es el que ejecuta la parte central de la tarea. Ahora bien : ¿ Que necesita Trabaja() para hacer su tarea ?.


Necesita una lista de objetos. De alli que Trabaja() tiene como parámetro un contenedor genérico de objetos a procesar.-

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;


namespace ThreadPoolTest_5
{
class MotorBatch
{

public static void trabaja(List <> cm)
{
//EL motor tiene su funcion de disparo y arma un
// handler a ella
WaitCallback callBack;
callBack = new WaitCallback(FuncionAEjecutar);

//El motor ajusta la cantidad de threads por procesador
int minWorker, minIOC;
ThreadPool.GetMinThreads(out minWorker, out minIOC);
// 4 threads asincronicos como minimo, por procesador
ThreadPool.SetMinThreads(4, minIOC);

//El motor lanza las tareas
foreach(object com in cm)
{
ThreadPool.QueueUserWorkItem(callBack, com);
}

//El motor sincroniza el pool de threads
//Hasta terminar
Console.ReadLine();
}

private static void FuncionAEjecutar(object mc)
{
Comando xco = (Comando)mc;
xco.inicio();
xco.run();
xco.cleanup();
}

}//clase

}//namespace




Un programa que utiliza el motor batch

Corresponde ahora que hagamos un programa que utiliza el motor batch para dispara procesos. Lo que haremos será que el motor dispare las implementaciones de Comando que hicimos con miClase y miOtraClase.-
Observemos que es lo que nuestro programa hace ahora:
Siendo que el motor necesita que a su método publico y estático Trabaja() se le pase como parámetro un genérico de tipo con objetos, lo primero que hace la aplicación que llamara al motor es generar una lista de dicho tipo.
Podemos ir viendo ya que esta generación es una tarea que debería estar controlada en el sentido que no podemos colocar cualquier tipo de objetos en esa lista sino solamente aquellos que deriven de Comando. Ese punto lo revisaremos en las mejoras que haremos al motor batch.-



using System;
using System.Threading;
using System.Collections.Generic;

namespace ThreadPoolTest_5
{

//
class MainApp
{
static void Main()
{
//Alguien carga los comandos en una lista y el motor
//la invoca
List <> cm = ListaDeComandos();

MotorBatch.trabaja(cm);

}

static public List ListaDeComandos()
{
miClase mc;
miOtraClase moc;
List<> comandos = new List<> ();

for (int i = 0; i <>

{

if ((i % 2) != 0)

{

mc = new miClase();

mc.nombre = "Hilo nro: " + i; comandos.Add(mc);

}

else

{

moc = new miOtraClase();

moc.nombre = "Hilo nro: " + i;

comandos.Add(moc);

}

}//for..


return comandos;

}//

}

}





Vemos en esta sencillo ejemplo las ideas basixas del motor batch y su rapida implementaciom
Dejamos para una IV parte de esta serie una version con mejoras en el diseño del motor
**************************************
[1] GoF es el acrónimo de “Gang of Four” o sea “La banda de los cuatro”. El nombre es una broma que liga la denominación de los 4 autores del libro Pattern Designs (Gamma, Helms, Jhonson y Vlissides)con la denominación de un grupo político del poder chino posterior a la muerte de Mao Tse Tung al que se lo acuso de traición por sus posturas radicalizadas durante la revolución cultural .-

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

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)