Manual del
Lenguaje de programación
C#
Parte V-A
Programación con hilos múltiples (Threads) y uso de Patrones de Diseño
Las clases que implementan la interfaz Comando 4
El motor batch de tareas asíncronas 8
Un programa que utiliza el motor batch 10
Una mejor versión del motor de procesos batch 12
La clase abstracta AbstractComando 15
Una mejora en el motor batch de tareas asincrónicas 16
OBJETIVO DEL MANUAL
Es intención en este manual presentar 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 interactivos ) 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 []). 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().-
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
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;
}
}//clase
}//namespace
La clase miOtraClase
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));
while (dFabsLRas > dFabsProd)
{
k = k + 2;
t = (-1) * t * RadCuadrado / (k * (k - 1));
// 1 - x^2/2! ...
s = s + t;
dFabsProd = (EPSILON * Math.Abs(s));
dFabsLRas = Math.Abs(t);
}
Aux = lRadianes.ToString() + ":" + s.ToString();
//Mostrar 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 al que también llamamos monitor.
Se trata 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:
- 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.-
- 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.-
- 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.-
- 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 <List> 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<object> 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 <List> 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<object> cm = ListaDeComandos();
MotorBatch.trabaja(cm);
}
static public List<object> ListaDeComandos()
{
miClase mc;
miOtraClase moc;
List<object> comandos = new List<object>();
for (int i = 0; i < 10; 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);
}
}
return comandos;
}//
}
}
Una mejor versión del motor de procesos batch
La Interfaz de Comandos
Si hacemos un análisis más meticuloso de la estructura que tendrá el motor batch, vemos que el diseño no solamente se adecua al uso de una interfaz (que es la representación de una plantilla con métodos comunes para polimorfismo) sino que podemos tener una clase abstracta con implementaciones de dichos métodos comunes.-
Estas implementaciones en algunos de los casos serán luego sobrescritas por las implementaciones de la clase abstracta y en otros casos serán implementados ya que algunos métodos los declararemos también como abstractos.-
Usando la utilidad View Class Diagram de Visual Studio, podemos ver las relaciones entre la Interfaz (y su declaración de métodos) con la clase abstracta y una clase (miClase) que implementa a esta ultima.-
Esta relación entre Interfaz-Clase Abstracta-Clase Concreta es un ejemplo de cómo debemos diseñar este tipo de problemas de ingeniería:
- Un problema requiere de la confección de un algoritmo que ejecuta acciones de objetos que tienen métodos comunes, pero se instancian de CLASES DISTINTAS. En nuestro caso el problema es la ejecución de los métodos de un comando.-
- Esto nos lleva a la idea de polimorfismo. Para eso entonces diseñamos la interfaz y pensamos los métodos comunes.-
- Existen además métodos que admiten cierta implementación o directamente necesitan de ella. Otros métodos, en cambio, deben ser definido para cada clase concreta
- Esto nos lleva a la idea de clase abstracta y métodos virtuales. La clase será la base de otras que heredaran su comportamiento pero lo especializaran.-
- El diseño de una clase abstracta es una tarea de pasos sucesivos. No es el primer eslabón de la cadena sino algún punto de culminación ya que implica que hemos visualizado un patrón de comportamiento del sistema y que hay tareas que sabemos que son comunes.-
- Si hemos llegado a ese punto del diseño, podremos diferenciar entre aquellos métodos que serán implementados en la clase abstracta y posiblemente ampliados en la subclase, y aquellos otros métodos a los que NECESARIAMNETE implementara la subclase.
En nuestro caso, Inicio() y YaTermino() por ejemplo, han sido implementadas en la interfaz, en tanto presentan una lógica que entendemos común a todos los derivados.
public virtual void inicio()
{
_terminado = false;
}
public bool YaTermino()
{
return (_terminado == true);
}
EN cambio run() que es el proceso de ejecución en si de la clase, será necesariamente implementado por la subclase. De allí que coloquemos a este ultimo método como abstract.
public abstract void run();
La clase abstracta AbstractComando
Dentro del archivo donde declaramos la Interfaz Comando, colocaremos una clase Abstracta que contenga los métodos básicos y una implementación mínima de los mismos.-
Llamamos a esta clase AbstractComando
using System;
using System.Collections.Generic;
using System.Text;
namespace threadPoolTest_6
{
public interface Comando
{
void inicio();
void run();
void cleanup();
bool YaTermino();
String elNombre();
}
public abstract class AbstactComando:Comando
{
public static string CLASE_BASE = "threadPoolTest_6.AbstactComando";
private string _nombre;
private bool _terminado;
public string nombre
{
get { return _nombre; }
set { _nombre = value; }
}
public String elNombre()
{
return _nombre;
}
public virtual void inicio()
{
_terminado = false;
}
public bool YaTermino()
{
return (_terminado == true);
}
public abstract void run();
public virtual void cleanup()
{
_terminado = true;
}
}
}
Una mejora en el motor batch de tareas asincrónicas
using System;
using System.Collections.Generic;
using System.Collections;
using System.Text;
using System.Threading;
using System.Reflection;
namespace threadPoolTest_6
{
public class MotorBatch
{
public static void trabaja(List<object> 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 la finalizacion el pool de threads
do
{
foreach (object com in cm)
{
Comando xco = (Comando)com;
if (xco.YaTermino())
{
//TODO: Logger
Console.WriteLine("-> Comando '{0}' procesado. Restan '{1}'", xco.elNombre(), cm.Count);
cm.Remove(xco);
break;
}
}
} while (cm.Count > 0);
//Hasta terminar
//TODO:Logger
Console.WriteLine("Motor Batch Termino");
}
//Esta es la funcion a la que se apunta en el callback
//llama a los 3 metodos de un comando
private static void FuncionAEjecutar(object mc)
{
Comando xco = (Comando)mc;
xco.inicio();
xco.run();
xco.cleanup();
}
}//clase
public class unPar
{
String _nombreClase;
String _nombreYPasoAssembly;
public String nombreYPasoAssembly
{
get { return _nombreYPasoAssembly; }
set { _nombreYPasoAssembly = value;}
}
public String nombreClase
{
get { return _nombreClase; }
set { _nombreClase = value; }
}//-
public unPar(String nombreClase, String nombreYPasoAssembly)
{
_nombreClase = nombreClase;
_nombreYPasoAssembly = nombreYPasoAssembly;
}//-
}//clase
public class ParAssemblyClase
{
static List <unPar> lPar = null;
public static void Agregar(String nombreClase,String nombreYPasoAssembly)
{
if (lPar == null)
lPar = new List<unPar>();
unPar p = new unPar(nombreClase,nombreYPasoAssembly);
lPar.Add(p);
}//
public static List<unPar> TraerLista()
{
return lPar;
}//
//Retornar una lista de comandos a partir de una lista
// de pares Assembly-clase que se busca cargado en la misma sesion
static public List<object> ListaComandos()
{
List<object> comandos = null;
if (lPar != null)
comandos= ListaComandos(lPar);
return comandos;
}
//Retornar una lista de comandos a partir de una lista
// de pares Assembly-clase que se recibe
static public List<object> ListaComandos(List<unPar> tabla)
{
Assembly assem;
Object o;
List<object> comandos = new List<object>();
IEnumerator i = tabla.GetEnumerator();
unPar m;
while (i.MoveNext())
{
m = (unPar)i.Current;
try
{
assem = Assembly.LoadFrom(m.nombreYPasoAssembly);
//Los constructores de las clases que implementan
//Comando no tienen parametros. De alli el parametro
//new Object[] { }
o = assem.CreateInstance(m.nombreClase, false,
BindingFlags.ExactBinding, null, new Object[] { }, null, null);
//TODO: Logger
Console.WriteLine("Clase Base de {0} : {1}",m.nombreClase, o.GetType().BaseType.ToString());
//solo los que extienden AbstractComando son considerados
//procesos a aejecutar
if (o.GetType().BaseType.ToString().Equals( AbstactComando.CLASE_BASE))
comandos.Add(o);
}
catch (Exception ex)
{ //TODO: Logger
Console.WriteLine("Excepcion " + ex.StackTrace);
}
}
return comandos;
}//-
}//clase
}//namespace
No hay comentarios:
Publicar un comentario