Cualquier cosa que valga la pena se hace en equipo

Cualquier cosa que valga la pena se hace en equipo

viernes, 4 de junio de 2010

Programacion Reflexiva . PARTE III

Algo más al respecto

Los assemblies contienen módulos, los módulos contienen tipos y los tipos contienen funciones miembro o métodos.
-
Lo que Reflection provee son objetos que encapsulan a estas unidades (assemblies, módulos y tipos) de modo de crear dinámicamente una instancia de un tipo, ligar ese tipo a un objeto existente u obtener el tipo de un objeto existente.-

De este modo podemos invocar los métodos del tipo y/o acceder a sus campos y propiedades. El uso típico de reflection debería seguir estas reglas:

1. Usar la clase Assembly para definir y cargar assemblies, cargar los módulos que el assembly contiene y que se encuentran en el Manifiesto, ubicar el tipo del assembly en si y crear una instancia del mismo.-

Esto lo podemos hacer con:

Assembly a = Assembly.LoadFrom(args[0]);
O
Assembly a = Assembly.GetExecutingAssembly();

El primer modo es genérico y solo requiere que se indique el assembly. (El paso completo del mismo será obligatorio si no se encuentra en la GAC). El segundo método está restringido a el trabajo con clases que se encuentran en el assembly actual.-

2. Usar el método CreateInstance() para instanciar un objeto de un tipo dado:

Object o = assem.CreateInstance("Ejemplo2", false,
BindingFlags.ExactBinding, null, new Object[] { 2 }, null, null);


· Alternativamente observemos que esto puede hacerse así:

MethodInfo m = assem.GetType("Ejemplo2").GetMethod("MetodoDeEjemplo");
Object ret = m.Invoke(o, new Object[] { 42 });

El método GetType retorna un objeto de la clase MethodInfo.
Observemos que igual que en el caso anterior, el nombre de la clase y el de su metodo, SON LITERALES lo que indica que pdrian ser cargados en tiempo de ejecucion en variables string y la llamada a Assembly.GetType o a Assembly.CreateInstance se podria hacer con datos dinamicos.
Desde ya que esta es la funcion ultima de Reflection.-
Observe de todos modos que debe conocer “algo” del tipo o del metodo de modo de invocar correctamente a los constructores o pasar parametros adecuados a los metodos.-

EJEMPLO II

En este ejemplo haremos una variante que nos llevara a un caso más parecido al que planteamos al principio de esta y que mostramos con un ejemplo en lenguaje C.-
Construiremos 3 proyectos, ambos en la misma solucion solo a los efectos de la simplicidad.
Uno, al que llamaremos biblioteca1, correspondera al armado de una biblioteca donde estan las clases de las que crearemos objetos a los que a accederemos.-
El segundo, al que llamaremos pruebabiblioteca, sera una aplicacin intermedia de ayuda. Esto no es necesario para el proyecto final que es una aplicación que usando reflection hara uso de los metodos de la biblioteca. Pero es una biena idea probar la biblioteca SEPARADAMENTE antes de darle un uso generico. Es por eso que colocamos este proyecto dentro del ejemplo.-
El tercero, que es el que nos importa, sera la aplicación que a traves de Reflection accede a estos objetos y sus metodos en modo dinamico.-

El proyecto biblioteca1

El primer proyecto sera del tipo Class Library y en el colocaremos una clase con dos metodos.-
Luego esta sera nuestra biblioteca armada como Assembly.-
Vamos con el primer proyecto que consiste en armar la biblioteca de la clase que expondra funcionalidad a ser utilizada por el segundo proyecto:
Este es un proyecto Class Library de modo de armar una biblioteca con una clase que exporte sus metodos.-

El codigo que escribiremos para crear tal clase es el que se muestra a continuacion:

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

namespace biblioteca1
{
public class UnaClase
{
public static Boolean SinNumeros(string dato)
{
char []numeros = new char[]{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
if (dato.LastIndexOfAny(numeros) == -1)
return (true);
else
return (false);
}

public static Boolean SinÑuflos(string dato)
{
char []ñuflos = new char[] { '@', '^', '`', '¨', '~' };
if (dato.LastIndexOfAny(ñuflos) == -1)
return (true);
else
return (false);
}

}
}


Ahora vamos a armar la biblioteca (biblioteca1) como una dll que expondrá la clase UnaClase y sus métodos públicos.-
Esto generara el archivo biblioteca1.dll .-
Revise en la pantalla de salida donde queda generada:

El proyecto PruebaBiblioteca

Ahora vamos a construir el segundo proyecto

Vamos a hacer una aplicación de consola que utilice la biblioteca creada en el proyecto anterior.
Lo hara del modo habitual, es decir sin utilizacion de reflection.
EL codigo C# de este proyecto esta en un solo modulo al que llamaremos Program y es el que se ve a continuacion:

using System;
using System.Collections.Generic;
using System.Text;
using biblioteca1;

namespace PruebaBiblioteca
{
class Program
{
static void Main(string[] args)
{
string dato = "ABCDE";
if (biblioteca1.UnaClase.SinNumeros(dato) == true)
Console.WriteLine("{0}: No tiene numeros");
else
Console.WriteLine("{0}: Tiene numeros");

dato = "AB1C2DE";
if (biblioteca1.UnaClase.SinNumeros(dato) == true)
Console.WriteLine("{0}: No tiene numeros");
else
Console.WriteLine("{0}: Tiene numeros");

dato = "A~CDE";

if (biblioteca1.UnaClase.SinÑuflos(dato) == true)
Console.WriteLine("{0}: No tiene Ñuflos");
else
Console.WriteLine("{0}: Tiene Ñuflos");

}
}
}

Programacion Reflexiva . PARTE II



El enfoque en C#
El cargador del lenguaje común administra lo que se denominan DOMINIO de APLICACIONES. El lenguaje común es el CONTENEDOR que aísla al programa del SO. Cualquier aplicación desarrollada en .NET no se comunicara directamente con APIs de WIN32, tal como lo hacíamos anteriormente sino a través de funciones, servicios, etc. del contenedor.- EL DOMINIO de la APLICACIÓN es el contorno o los limites en los que trabajan aquellos objetos que pertenecen al mismo alcance de aplicación.- La administración de este cargador incluye la carga de cada assembly en el dominio de aplicación adecuado y controla la disposición y arreglo en memoria del tipo de jerarquía en cada assembly.-
Veamos unos ejemplos.-

Ejemplo I:
En este caso haremos un proyecto que contendra las clases que queremos acceder a traves de Reflection.- Es por eso que utilizamos el metodo estatico: Assembly.GetExecutingAssembly(); para poder tener un objeto de la clase Assembly. Este es el modo de hacerlo cuando lo que necesitamos es un objeto de una clase que esta dentro del codigo actualmente en ejecucion.- Obtenemos un objeto de la clase Assembly de este modo : Assembly assem = new Assembly()) Este el objeto creado nos permitira, por ejemplo, instanciar objetos de las clases contenidas en el assembly.- Para eso el primer paso es:


¿Como acceder a una clase del assembly?

El objeto de la clase Assembly tiene un metodo llamado CreateInstance().
Este método admite como parámetro el nombre de una clase así como los parámetros que necesitase su constructor.
Este método es uno de los métodos capitales de la programación reflexiva ya que nos permite a través de un texto que representa el nombre de la clase, obtener un objeto que es una instancia de la misma.-
El codigo siguiente muestra la utilizacion del metodo

CreateInstance():

// Crear un objeto de la clase Ejemplo2, desde el assembly que la contiene // El constructor de Ejemplo2 necesita un parametro que es un entero //

Object o = assem.CreateInstance("Ejemplo2", false, BindingFlags.ExactBinding, null, new Object[] { 2 }, null, null);

Observe que se accede a una clase y se instancia un objeto de la misma a traves de un parametro que es un texto ("Ejemplo2").-
Observe tambien que uno de los parametros de CreateInstance() es un array de Object al que se le envian los parametros QUE EL CONSTRUCTOR de la clase necesita.
Esto es: No es suficiente con conocer el nombre de clase en el assembly sino que es preciso conocer algo mas de ella.
Esto es similar a lo que nos sucede en el programa C que vimos al inicio en el que debemos conocer algo del PROTOTIPO de la funcion para hacer una llamda exitosa.-

¿Como acceder a un metodo de la clase?

Ahora bien, tenemos un objeto O que es una instancia de una clase del assembly. ¿Como hacemos ahora para acceder a un metodo de dicha clase?
El handler al assembly (assem) tiene un metodo llamado GetType que obtiene información sobre el tipo exacto que se produce en tiempo de ejecucion para el objeto corriente.-
Es decir GetType retorna un tipo (Type). La clase Type es la base de reflection para acceder a la metada del objeto. Type es una interfaz que tiene distintas implementaciones. La que aquí no sinteresa, tiene un metodo llamado GetMethod() que, tomando como parametro el nombre del metodo, nos trae información del mismo. Asi:

MethodInfo m = assem.GetType("Ejemplo2").GetMethod("MetodoDeEjemplo");

Obtenida la información sobre el metodo, se lo puede invocar a traves del metodo Invoke de MethodInfo .
Igual que en el caso anterior, si bien es posible obtener acceso al metodo a traves de un literal (en este caso el nombre del metodo es : MetodoDeEjemplo ), el uso posterior del metodo requiere conocer cuales son los parametros adecuados.

Object ret = m.Invoke(o, new Object[] { 42 });

En este caso el entero 42 se corresponde al tipo de datos que corresponde al Metodo, MetodoDeEjemplo EL retorno que produce Invoke es un Object.

EL uso posterior del mismo sera parte de la logica de negocios del programa.-

Arme un proyecto de consola simple y coloque el código indicado en un modulo cs (C#)
















Y pruebe este codigo:

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Security.Permissions;
using System.Configuration;



class Ejemplo2
{
private int factor;

public Ejemplo2(int f)
{
factor = f;
}

public int MetodoDeEjemplo(int x)
{
Console.WriteLine("\nEjemplo2.MetodoDeEjemplo({0}) ejecutado.", x);
return x * factor;
}

public string OtroMetodo(string a)
{

StringBuilder st = new StringBuilder("A este dato le concateno: ");
Console.WriteLine("\nEjemplo2.OtroMetodo({0}) ejecutado.", a);
st.Append(a);
return st.ToString();
}


}

public class ejecuta
{

public static void Main()
{

Assembly assem = Assembly.GetExecutingAssembly();

Console.WriteLine("Nombre completo del Assembly:");
Console.WriteLine(assem.FullName);

//
AssemblyName assemName = assem.GetName();
Console.WriteLine("\nName: {0}", assemName.Name);
Console.WriteLine("Version: {0}.{1}",
assemName.Version.Major, assemName.Version.Minor);

Console.WriteLine("\nAssembly CodeBase:");
Console.WriteLine(assem.CodeBase);

// Crear un objeto de la clase Ejemplo2, desde el assembly que la contiene
//El constructor de Ejemplo2 necesita un parametro que es un entero
//
Object o = assem.CreateInstance("Ejemplo2", false,
BindingFlags.ExactBinding,
null, new Object[] { 2 }, null, null);

// Ahora apuntamos a un metodo del objeto:

MethodInfo m = assem.GetType("Ejemplo2").GetMethod("MetodoDeEjemplo");
Object ret = m.Invoke(o, new Object[] { 42 });
Console.WriteLine("MetodoDeEjemplo retorno: {0}.", ret);

MethodInfo m2 = assem.GetType("Ejemplo2").GetMethod("OtroMetodo");
ret = m2.Invoke(o, new Object[] { "Este Texto" });
Console.WriteLine("OtroMetodo retorno: {0}.", ret);

Console.WriteLine("\nPunto de entrada del Assembly:");
Console.WriteLine(assem.EntryPoint);
}
}

Ejecute el codigo y vera que nos muestra lo siguiente:

El nombre del assembly
El metodo que hemos ejecutado y el valor que retorna el mismo
El punto de entrada del assembly

La programacion Reflexiva-PARTE I

Reflection

Para la ejecucion de una aplicación .NET debemos tener presente el concepto de CONTENEDORES. Este es un concepto que se ha popularizado los ultimos años a traves de Java y difiere de los habituales run-times en que no es un agregado que lleva el ejecutable o un conjunto de bibliotecas que resuelven aspectos especificos (como graficos), SINO QUE ES UNA MAQUINA VIRTUAL dentro de la cual corre la aplicación.-
Es decir esta cascara es la que separa a la aplicación del Sistema operativo.-
Sus ventajas en el mundo Java son obvias en tanto estos contenedores aislan al programador de especificidades de tal o cual sistema operativo.-
No nos resultan tan obvias en el mundo Microsoft donde existe UN SOLO sistema operativo.-
Sin embargo hay otras decisiones que seguramente habrán pesado en la decisión de Microsoft de hacer un esquema de contenedores.-
El primero es que este esquema permite que exista un nivel de compilación que no es la generación del ejecutable mismo, sino de un lenguaje intermedio (MSIL para Microsoft, Bytecodes para Java y… P-CODE para quienes tuvimos oportunidad de trabajar profesionalmente con el maravilloso Pascal ¡!).-
El segundo es que varios lenguajes (al menos Visual Basic, C# y C++ en modo manejado) pueden trabajar sobre esta maquina virtual y compartir aquellos servicios, bibliotecas y assemblies que la maquina virtual necesita. Esto es un enorme avance desde el punto de vista de cómo cada lenguaje expone funcionalidades comunes a los programadores.-
Tambien aca podemos decir que esto existía, para determinados servicios, en los sistemas operativos propietarios de Digital e IBM.-
Sobre todo en los del primero, su sistema operativo VMS (hoy propiedad de Hewlett Packard) presentaba para la familia de lenguajes (Fortran, C, Cobol y Basic) idénticos nombres de rutinas para todo el sistema de I/O y de archivos ISAM.-
Solo que en estos casos un runtime que se adjuntaba al programa compilado era el encargado de proveer esta funcionalidad comun. Esto por supuesto duplicaba el codigo en cierto punto, aunque los servicios que utilizaba el runtime estaban en el kernel del SO por lo que no era tan problemático.-
¿Como funciona esto y para que sirve realmente?
Vamos a hacer un par de aplicaciones que nos muestren como funciona esto.
Antes de avanzar nos gustaría que repasemos como, en un lenguaje como el C, resolvemos un problema común:
Queremos invocar un método, que esta en una biblioteca CUYO ENLACE no se produce cuando compilamos, sino en tiempo de ejecución.-
Esto da a nuestros programas otra versatilidad. Es decir nuestro programa es capaz de:
Cargar una biblioteca a traves de su nombre LITERAL. Es decir un string en tiempo de ejecucion (por ejemplo leido de un archivo de configuración) dira cual es la biblioteca a a cargar)
Colocar un apuntador a una funcion de dicha biblioteca (definible tambien como un string)
Hay alguna habilidad que debe tener el SO en el que trabajamos y es la de aceptar bibliotecas compartidas. En Windows estas tienen la extension .DLL y en UNIX la extension .SO
Es decir la biblioteca que invocaremos desde nuestra aplicación sera del tipo compartida (Shared) y no del tipo estatica (STATIC).-
El ejemplo lo haremos para UNIX solo para ver alguna pequeña diferencia de cómo lo hariamos en Windows.-
El programa que haremos sabra cargar una biblioteca por nombre. En nuestro caso los nombres son lib1.so y lib2.so. Estos nombres son totalmente arbitrarios y podrian ser cualquier otro valido para el Sistema operativo.-
El programa cargara alguna de estas 2 bibliotecas y llamara la funcion saludo().
Esta funcion existe en ambas bibliotecas y el programa es capaz de trabajar con esta redefinicion (override) ya que dependera de la biblioteca que cargue lo que determinara cual es la tarea que termine realizando.-
Este es el codigo de una biblioteca escrito en ANSI C:

#include "stdio.h";
#include "dlfcn.h";/* bibliotecas de manejo dinamico de funciones:
dlopen(), dlclose(), dlsym() ... */

/* Explicamos como usar el programa. */
void usage(int argc, char* argv[])
{
fprintf(stderr, "Usar asi: %s [12] a los efectos de llamar a la lib1 o a la lib2 \n", argv[0]);
exit(1);
}

int main(int argc, char* argv[])
{
int lib_num;
/* Que biblioteca usar ? */
char biblioteca[100]; /* nombre del archivo de la biblioteca */
void* lib_handle; /* manejador para la biblioteca compartida cargada*/
void (*fn_de_la_biblioteca)(); /* puntero a UNA FUNCION de la biblioteca*/
const char* error; /* por si hay errores... */

/* necesito un argumento */
if (argc != 2)
usage(argc, argv);
/* Chau!! */

/* Esta es la biblioteca a cargar. */
lib_num = atoi(argv[1]); /* */

if (lib_num < 1 lib_num > 2)
usage(argc, argv); /* Chau !! */

/* ahora armo el nombre de la biblioteca concatenando la palabra "lib" con el numero 1 o 2
para obtener lib1.so o lib2.so */
sprintf(biblioteca, "lib%d.so", lib_num);

/* carga la biblioteca que desea (lib1.so o lib2.so) */
lib_handle = dlopen(biblioteca, RTLD_LAZY);



if (!lib_handle)
{
fprintf(stderr, "Error: %s\n", dlerror());
exit(1);
}

/* cargar la funcion saludo QUE ESTA EN CUALQUIERA
DE LAS DOS BIBLIOTECAS !*/
fn_de_la_biblioteca = dlsym(lib_handle, "saludo");

error = dlerror();
if (error) {
fprintf(stderr, "Error: %s\n", error);
exit(1);
}

/* Llamar a la funcion. */
(*fn_de_la_biblioteca)();

/* liberar el recurso */
dlclose(lib_handle);

return 0;
}

Este modulo es LIB1.SO:

#include ;

/* Esta es la funcion de la biblioteca*/
void saludo()
{
printf("Que puedo decir que no sea HOLA MUNDO?\n");
}

/* cleanup de la biblioteca */
void _fini()
{
printf("Limpiando la biblioteca 'lib2.so'\n");
}

Este modulo es LIB2.SO:
#include ;

/* funcion de inicializacion: OBLIGATORIO llamarla '_init'. */
void _init()
{
printf("Inicializando biblioteca 'lib1.so'\n");
}

/* Esta es lo funcion que nos importa */
void saludo()
{
printf("HALOAA !!\n");
}

Como vemos este problema responde a un patron comun: La resolucion en tiempo de ejecucion de una funcionalidad determinada.-
Los lenguajes modernos como C# y Java resuelven esto con el paradigma denominado programacion reflexiva.
Como en muchos otros conceptos de estos lenguajes modernos, las ideas se generaron en Smalltalk y C++.-

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)