octubre 2007 - Posts
Siguiendo con el el artículo anterior de la memoria en .NET donde explicaba como esta estructurada, sigo con las variables, que en .NET principalmente son de dos tipos:
| Tipo (alias) | Bytes | Rango |
| Char (char) | 2 | Caracteres |
| Bolean (bool) | 4 | True ó False |
| IntPtr | ? | Puntero nativo |
| DateTime (date) | 8 | 1/1/0001 12:00:00 AM a 12/31/9999 11:59:59 PM |
| SByte (sbyte) | 1 | -128 a 127 |
| Byte (byte) | 1 | 0 a 255 |
| Int16 (short) | 2 | -32768 a 32767 |
| Int32 (int) | 4 | -2147483648 a 2147483647 |
| UInt32 (uint) | 4 | 0 a 4294967295 |
| Int64 (long) | 8 | -9223372036854775808 a 9223372036854775807 |
| Single (float) | 4 | -3.402823E+38 a 3.402823E+38 |
| Double (double) | 8 | -1.79769313486232E+308 a 1.79769313486232E+308 |
| Decimal (decimal) | 16 | -79228162514264337593543950335 a 79228162514264337593543950335 |
- Se conoce su tamaño antes de su inicialización, el más grande tiene 16 Bytes.
- Heredan de System.ValueType y ninguno puede ser extendido (sealed)(ni siquiera ValueType).
- Se almacenan en el thread stack cuando son variables locales y expresiones intermedias, y en el managed heap en el resto de situaciones (variables globales ó/y estáticas).
- Las estructuras (struct) y enumeraciones (enum) son tipos por valor también y responden al mismo comportamiento.
- La memoria ocupada por estos tipos se elimina en cuanto están fuera de ámbito en caso de estar en el thread stack, y por medio del GC cuando estan en el managed heap (desapareciendo junto con la clase a la que está asociada).
- Tipos por referencia (objeto):
- No se conoce su tamaño hasta después de su inicialización.
- Todos los demás tipos de la BCL.
- Heredan directamente de System.Object, y pueden ser extendidos a no ser que se especifique lo contrario (sealed).
- Se almacenan siempre en el managed heap, y son accedidos desde el thread stack mediante una referencia.
- Cuando declaramos una variable de tipo objeto, se crea una referencia en el thread stack, y al crear la instancia en el managed heap por medio del operador new, su dirección en memoria se asigna a la susodicha variable. De esta forma se puede acceder al objeto por medio de la indirección de la referencia.
- La memoria ocupada por estos tipos la libera el GarbageCollector de forma no determinística.
Si, no me he equivocado, los tipos por valor no se almacenan siempre en el thread stack :D, como decía solo se almacenan ahí cuando son variables locales y expresiones intermedias.
Como decía en el artículo anterior, el thread stack es un espacio de almacenamiento exclusivo de su hilo de ejecución y no puede ser compartido, sin embargo... te pones a pensar y te das cuenta de que puedes compartir variables de tipo por valor entre threads... por lo que lo primero que te viene a la mente es que este realizando boxing/unboxing para ello... pero si precisamente una de las metas del performance es evitar esas dos operaciones ... no tiene sentido!
Menos mal que tiene uno el .NET Reflector siempre a mano para salir de la penumbra xD
Vamos a ver como se comporta el CLR con una variable local y una variable externa de otra clase, ambas de tipo por valor:
class Program
{ static void Main(string[] args)
{ MyClass exampleClass = new MyClass();
exampleClass.globalInt = 6;
Int32 localInt = 5;
Console.WriteLine(localInt);
Console.WriteLine(exampleClass.globalInt);
}
}
class MyClass
{ public Int32 globalInt;
}
localInt es un Int32 declarado localmente y debería estar en el thread stack, y MyClass.globalInt es un Int32 declarado como miembro de un tipo por referencia, por lo que debería estar en el managed heap... vamos a verlo. El método Main genera el siguiente código CIL (obviadas las instrucciones que no interesan ahora):
1: .locals init (
2: [0] class Test.MyClass exampleClass,
3: [1] int32 localInt)
4: L_0000: newobj instance void Test.MyClass::.ctor()
5: L_0005: stloc.0
6: L_0006: ldloc.0
7: L_0007: ldc.i4.6
8: L_0008: stfld int32 Test.MyClass::globalInt
9: L_000d: ldc.i4.5
10: L_000e: stloc.1
11: L_000f: ldloc.1
12: L_0010: call void [mscorlib]System.Console::WriteLine(int32)
13: L_0015: ldloc.0
14: L_0016: ldfld int32 Test.MyClass::globalInt
15: L_001b: call void [mscorlib]System.Console::WriteLine(int32)
16: L_0020: ret
Lo primero que uno mira es si aparecen las instruciones box/unbox para realizar boxing/unboxing, pero como vemos no aparecen, por lo cual, se esta accediendo y asignando a una variable de tipo por valor en el heap sin realizar ninguna de estas dos operaciones.
Voy a explicarlo brevemente:
- Asignación del miembro de MyClass, líneas 7 y 8: carga en valor '6' en pila y lo asigna mediante la instrucción stfld, que precisamente sirve para asignar valores a una variable contenida en un objeto.
- Asignación de la variable local localInt, líneas 9 y 10: carga el valor '5' en pila y lo asigna mediante la instrución stloc a la variable local en el puesto 1 (la 0 es Test.MyClass como se puede ver en las 3 primeras líneas).
Queda claro que el tratamiento es distinto y que stfld sirve para manipular tipos por valor asociados a una instancia en el managed heap, de hecho si lo piensas bien, una vez que has localizado la instancia en el managed heap por medio de la referencia ... ¿porque hacer boxing/unboxing con ella? ... :P
Al igual pasa si lo hacemos con un campo estático, hay una instrucción especial para ello:
class Program
{ static Int32 intTest;
static void Main(string[] args)
{ intTest = 6;
Console.WriteLine(intTest);
}
}
CIL:
1: L_0000: ldc.i4.6
2: L_0001: stsfld int32 Test.Program::intTest
3: L_0006: ldsfld int32 Test.Program::intTest
4: L_000b: call void [mscorlib]System.Console::WriteLine(int32)
5: L_0010: ret
Como podemos ver utiliza otra instrución distinta, stsfld, para tratar con variables estáticas.
.NET Reflector da una orientación de que significa cada instrucción.
Así que vuelvo a repetir, los tipos por valor no se almacenan siempre en el thread stack como se viene diciendo en muchos sitios, solo se almacenan ahí cuando son variables locales y expresiones intermedias. El CIL tiene sus propias instruciones para acceder a tipos por valor cuando se encuentran en el managed heap, además del boxing/unboxing para almacenarlos allí directamente.
Próximo capítulo... boxing/unboxing a fondo.
Crossposting from vtortola.NET
Con este, quiero empezar una serie de pequeños artículos sobre como esta estructurada la memoria en .NET, sin profundizar en demasiados detalles... pero empezar por lo más simple y acabar hablando sobre temas de sincronización, atomicidad y volaticidad pasando por el GarbageCollector, intentando aportar una información que a mi parecer, todo desarrollador debería tener en mente a la hora de programar... y que se debe conocer en profundidad para desarrollar aplicaciones multithreading. Espero que sea útil para quien lo lea y personalmente me ayude a comprenderlo mejor.
La memoria de la que hacen uso nuestras aplicaciones administradas se divide en dos partes principalmente:
- Thread stack:
- Espacio de memoria asociado al hilo de ejecución.
- Es la pila donde va progresando nuestro código.
- Aquí se van "apilando" las llamadas a funciones y las variables locales y parámetros, de forma que el puntero de ejecución va cargando, ejecutando y liberando métodos con sus respectivas variables.
- No se pueden compartir ese espacio entre varios hilos, esta únicamente ligado un hilo de ejecución.
- Como decía es el espacio de memoria asociado al hilo de ejecución, por lo que es bastante rápido.
- Está limitado a un máximo de 1MByte por hilo.
- Si se intenta superar el límite de memoria se obtiene un StackOverflowException.
- Aquí se almacenan tipos de los cuales se conoce su tamaño antes de su inicialización (tipos por valor) y referencias a objetos del managed heap, que contienen la dirección de memoria del objeto allí ó un nulo si no referencian nada.
- El proceso de liberar memoria de este espacio se realiza de forma determinística por el puntero de ejecución.
- Managed Heap:
- Espacio de memoria asociado al proceso.
- Compartido entre los hilos que lo forman (si hubiese varios).
- Accedido mediante punteros (dirección/indirección) lo que lo hace más lento que el thread stack, pero permite ser accedido desde otros hilos.
- No tiene una limitación en su tamaño que no sea la del hardware.
- Normalmente almacena tipos por referencia y tipos por valor cuando tienen una relación de composición con un tipo por referencia.
- Si se superar el máximo de memoria disponible se obtiene un OutOfMemoryException.
- El proceso de liberar memoria de este espacio se realiza de forma no determinística por el recolector de basura (GarbageCollector).
Como se puede ver, nuestra ejecución reside en el thread stack (ya que MSIL es stack based), el puntero de ejecución va cargando lo que necesita para la ejecución y liberandolo en cuanto acaba. El problema viene cuando utilizamos tipos de los que no podemos saber la memoria que ocupan hasta después de su inicialización, entonces dependemos de otra zona de memoria "ilimitada" y de acceso mediante punteros llamada managed heap donde instanciamos dicho tipo, de forma que nuestro puntero de ejecución tiene una referencia en el thread stack apuntando a esa instancia pero no la instancia en si, lo que hace más lento el acceso al tener que recurrir a la indirección de la referencia.
Cuando hablamos de una referencia, estamos hablando de un puntero seguro y tipado, de forma que no puede apuntar a una dirección de memoria cualquiera, solo a la ubicación en memoria de una instancia de un tipo dado ó ser nulo(null).
El managed heap, al contrario del thread stack, puede ser compartido por varios hilos, lo cual no significa que pueda ser libremente compartido. Siempre que se vaya a trabajar con una instancia suceptible de ser accedida por múltiples hilos... es necesario sincronizar el acceso para evitar condiciones de anticipación y/ó dejarla en un estado inconsistente. Además, la memoria de este espacio no puede ser liberada de forma determinista, debe delegarse en un proceso en segundo plano llamado recolector de basura (GarbageCollector) encargado de liberar la memoria usada por instancias que no son referenciadas desde ningún thread stack.
En el próximo capítulo... las variables, tipos por valor y referencia.
Crossposting desde vtortola.NET
Por invitación de Rodrigo Corral a partir de ahora también postearé en Geeks.ms, comunidad que sigo desde hace tiempo y que es todo un privilegio para mi colaborar en ella.
Me llamo Valeriano, actualmente vivo en Madrid, trabajo en Avanade y llevo unos dos años y medio dándole a esto del .NET con C#. Tengo especial curiosidad por la programación concurrente que espero poco a poco ir dominando, por el .NET framework y C# en general. Espero compartir con vosotros todo lo que vaya aprendiendo sobre estos temas :D
Un saludo.
Esta clase, fruto de un fin de semana de inspiración y una semana de depurado en mis ratos libres, que no son muchos, sigue el planteamiento anterior para recorrer estructuras en arbol con múltiples hilos. Se trata de una clase abstracta que encapsula todo el mecanismo multithreading, simplemente hay que implementar un método que define como se obtienen sus hijos y suscribirse a un evento que es disparado por cada nodo.
Siempre es buena idea encapsular funcionalidades complejas de este tipo ya que:
- Permite probar la funcionalidad en otros campos.
- Permite hacer correciones de forma más sencilla. La lógica puede ser bastante complicada y no conviene mezclarla con la lógica de negocios.
- Permite su reusabilidad.
La clase utiliza un tipo genérico para que pueda adaptarse a cualquier tipo. Los nodos encontrados se envian vía evento, junto a un StringBuilder que indica su jerarquia en el arbol y cuyo separador puede ser indicado también en una sobrecarga del constructor. Además, la clase argumento del evento está implementado dentro de la clase, lo que permite que use el tipo genérico de la clase ThreadedTreeBase. Las excepciones capturadas se devuelven también en un evento, por lo que conviene comprobar si la propiedad Error es null ó no cuando evaluemos un nodo.
[more]
Hay un método que permite cancelar la operación si esta en curso por medio de la evaluación de una variable run que se comprueba en cada ciclo.
Cuando se construye, se le indica el número de hilos que podrá manejar y la prioridad que se le quiere asignar.
public abstract class ThreadedTreeBase<NodeType>
where NodeType : class
{ // Objeto que controla el fin de la ejecución
private readonly Object endLock = new Object();
// Delegado para recursividad
private delegate void runDlgt_(NodeType Node, StringBuilder Path);
private readonly runDlgt_ runDlgt;
// Control de hilos.
private readonly Int32 maxThreads;
private Int32 runningThreads = 0;
public Int32 MaxThreads { get { return maxThreads; } }
// Indicador de nodos por procesar
private Int32 remainingNodes = 0;
// Control de prioridad de ejecución.
private readonly ThreadPriority priority;
public ThreadPriority Priority { get { return priority; } }
// Control de cancelación.
private Int32 runFlag = 1;
// Caracter delimitador de la ruta xpath
private readonly Char xpathDelimiter = '/';
public Char XpathDelimiter { get { return xpathDelimiter; } }
// Evento de nodo procesado
protected delegate void ProcessedNode_(ProcessedNodeEventArgs e);
protected event ProcessedNode_ ProcessedNode;
protected class ProcessedNodeEventArgs : EventArgs
{ private NodeType node;
private String xpath;
private Exception error;
public NodeType Node { get { return node; } } public String XPath { get { return xpath; } } public Exception Error { get { return error; } }
public ProcessedNodeEventArgs(NodeType Nodo,
String XPath,
Exception Error)
{ this.node = Nodo;
this.xpath = XPath;
this.error = Error;
}
}
/// <summary>
/// Constructor protegido.
/// </summary>
/// <param name="MaxThreads">Máximo de tareas concurrentes.</param>
/// <param name="Priority">Indica la prioridad de los hilos.</param>
protected ThreadedTreeBase(Int32 MaxThreads, ThreadPriority Priority)
{ runDlgt = new runDlgt_(Run);
// Resto un hilo ya que hay que
// contar con el thread que inicia
// la clase
this.maxThreads = MaxThreads-1;
this.priority = Priority;
}
protected ThreadedTreeBase(Int32 MaxThreads, ThreadPriority Priority,
Char XpathDelimiter): this(MaxThreads, Priority)
{ this.xpathDelimiter = XpathDelimiter;
}
/// <summary>
/// Iniciar navegación del arbol.
/// </summary>
/// <param name="Parte">Parte inicial.</param>
public void RunBrowser(NodeType nodo)
{ lock (endLock)
{ runDlgt.BeginInvoke(nodo, new StringBuilder(),
delegate(IAsyncResult ia)
{ try { runDlgt.EndInvoke(ia); } catch (Exception Ex)
{ Debug.Fail(Ex.Message, Ex.StackTrace);
sendNodeData(nodo ?? default(NodeType), null, Ex);
}
}, null);
Monitor.Wait(endLock);
}
}
/// <summary>
/// Inicia recorrido desde nodo inicial.
/// </summary>
/// <param name="nodo">Nodo inicial</param>
private void Run(NodeType Node, StringBuilder path)
{ // Compruebo la cancelación (nuevas bifurcaciones).
if (Thread.VolatileRead(ref runFlag) == 0) return;
// Asigno la prioridad al thread.
Thread.CurrentThread.Priority = this.priority;
// Obtengo la jerarquia
path = new StringBuilder(path.ToString());
path.AppendFormat("{0}{1}",this.xpathDelimiter, Node);
// Envio el nodo vía evento
sendNodeData(Node, path, null);
// Obtengo los subnodos y los contabilizo
NodeType[] childs = getNodeChilds(Node);
lock (endLock) remainingNodes += childs.Length;
// Recorro la lista de nodos hijos...
foreach (NodeType child in childs)
{ // Compruebo la cancelación dentro del bucle.
// (bifurcaciones en curso).
if (Thread.VolatileRead(ref runFlag) == 0) return;
// ... llamando recursivamente a cada uno de ellos..
if (Thread.VolatileRead(ref runningThreads) < maxThreads)
{ // Incremento el número de hilos secundarios en ejecución.
Interlocked.Increment(ref runningThreads);
// Ejecuto en otro hilo.
runDlgt.BeginInvoke(child, path,
delegate(IAsyncResult ia)
{ try
{ Monitor.Enter(endLock);
runDlgt.EndInvoke(ia);
}
catch (Exception Ex)
{ Debug.Fail(Ex.Message, Ex.StackTrace);
NodeType userState = ia.AsyncState as NodeType;
sendNodeData(userState ?? default(NodeType), null, Ex);
}
finally
{ runningThreads--;
remainingNodes--;
// Se pulsa sobre 'endLock' si no hay hilos
// secundarios en ejecución ni nodos pendientes.
if ((runningThreads == 0) && (remainingNodes == 0))
{ Monitor.Pulse(endLock);
}
Monitor.Exit(endLock);
}
}, child);
}
else
{ try
{ runDlgt.Invoke(child, path);
lock (endLock)
{ remainingNodes--;
if ((runningThreads == 0) && (remainingNodes == 0))
{ Monitor.Pulse(endLock);
}
}
}
catch (Exception Ex)
{ Debug.Fail(Ex.Message, Ex.StackTrace);
sendNodeData(child ?? default(NodeType), null, Ex);
}
}
}
}
/// <summary>
/// Envia los datos vía evento.
/// </summary>
/// <param name="nodo">Nodo del que se obtuvo la información.</param>
/// <param name="infoNodo">Información.</param>
private void sendNodeData(NodeType nodo,
StringBuilder path, Exception Ex)
{ if(ProcessedNode!=null)ProcessedNode.Invoke(new ProcessedNodeEventArgs
(nodo, path == null ? String.Empty : path.ToString(), Ex));
}
/// <summary>
/// Cancela la ejecución.
/// </summary>
protected void CancelBrowse()
{ // Full memory barrier
Interlocked.Exchange(ref runFlag, 0);
}
/// <summary>
/// Función a implementar con la forma de obtener
/// los nodos hijos de otro nodo.
/// </summary>
/// <param name="nodo">Nodo padre</param>
/// <returns>Lista de nodos</returns>
protected abstract NodeType[] getNodeChilds(NodeType nodo);
}
Ahora un ejemplo de su implementación. El sistema de archivos es a fin de cuentas, una estructura en arbol, así que voy a hacer una pequeña implementación de esta clase que me diga cuantos archivos contiene un directorio contando los que hay en sus subdirectorios... en multihilo, por supuesto :D
Evidentemente, no tiene utilidad práctica lanzar múltiples hilos contra un recurso local, con latencia mínima y compartido para todos los hilos ... simplemente como prueba:
public class ThreadedTreeFileSystem
: ThreadedTreeBase<FileSystemInfo>
{ // Contador
private long counter = 0;
public Int64 Contador
{ get { return Thread.VolatileRead(ref counter); } }
public ThreadedTreeFileSystem(int MaxThreads, ThreadPriority p)
: base(MaxThreads, p)
{ // Me subscribo al evento que devuelve los nodos.
this.ProcessedNode += new ThreadedTreeBase<FileSystemInfo>.ProcessedNode_
(ExplosionSistemaDeArchivos_ProcessedNode);
}
void ExplosionSistemaDeArchivos_ProcessedNode
(ThreadedTreeBase<FileSystemInfo>.ProcessedNodeEventArgs e)
{ // Si el nodo es un archivo, lo cuento y lo muestro.
if (e.Node is FileInfo)
Interlocked.Increment(ref counter);
}
protected override FileSystemInfo[] getNodeChilds(FileSystemInfo nodo)
{ // Obtengo la lista de subnodos.
List<FileSystemInfo> array = new List<FileSystemInfo>();
try
{ if (nodo is DirectoryInfo)
{ array.AddRange(((DirectoryInfo)nodo).GetDirectories("*")); array.AddRange(((DirectoryInfo)nodo).GetFiles());
}
}
catch (SecurityException sEx)
{ Console.WriteLine("{0} : {1}", sEx.GetType().Name, sEx.Message); }
return array.ToArray();
}
}
La implementación tiene cuatro partes esenciales:
- La clase se define heredando de ThreadedTreeBase y especificando el tipo que usaremos como nodo.
- En el constructor, se pasan los parámetros adecuados y se define el evento.
- En el manejador del evento, se procesa el nodo, en este caso, únicamente se cuentan los archivos y se muestra su nombre. También podríamos ver la jerarquia en e.XPath.
- En GetNodeChilds, el método que debemos implementar para completar la clase abstracta, esta la lógica necesaria para obtener los subnodos de un nodo y lo devuelve en forma de array.
Es importante remarcar, que aunque todo el mecanismo multithreading esta encapsulado, seguimos necesitando sincronizar los accesos a la memoria compartida, como por ejemplo el acceso a counter, que se incrementa con Interlocked para evitar race conditions (condiciones de anticipación).
Para su uso...
string folder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
ThreadedTreeFileSystem browser =
new ThreadedTreeFileSystem(5, ThreadPriority.Normal);
browser.RunBrowser(new DirectoryInfo(folder));
Console.WriteLine("-- Archivos: " + browser.Contador);
Cualquier feedback sobre esta clase ó su diseño es bienvenida :)
Una estructura arbol se recorre por medio de una función recursiva empezando desde el nodo raiz por medio de dos operaciones básicas, un método ProcessNode con el que procesamos la información de ese nodo, y otro GetNodeChilds con el que obtenemos los nodos hijos de un nodo, sería algo así:
static void BrowseTree(Node root)
{ ProcessNode(root);
foreach (Node n in GetNodeChilds(root))
{ BrowseTree(n);
}
}
Pero cuando esa estructura en arbol es realmente grande, ó alguna de las dos operaciones conlleva una latencia significativa, como puede ser por ejemplo procesar cada nodo localmente u obtener los hijos de un nodo a través de la red, puede ser muy útil recorrer dicha estructura con múltiples hilos si se dan las condiciones adecuadas, que pueden ser:
[more]
- Latencia elevada que provoca tiempos de Idle significativos al obtener los hijos.
- Múltiples CPUs en la máquina que navega el arbol.
- Múltiples CPUs en la máquina que contiene el arbol, capacidad para atender peticiones concurentes y preferiblemente concurrencia optimista.
El algorrítmo se complica un poco, pero gracias al modelo de programación asíncrono podemos simplificar bastante. A priori, puede parece que es tan simple como lanzar un hilo por cada nodo hijo del nodo raiz, pero si el nodo raiz tiene muchos hijos en su primer nivel podemos dejar vacio el ThreadPool pudiendo causar un deadlock ó en caso de usar Threads convencionales llegar a un número contraproducente de ellos, por lo que debemos controlar cuantos hilos vamos a usar para recorrer el arbol. Pero no esta todo solucionado, si el primer nodo hijo tiene 4 hijos, y el segundo nodo hijo tiene 400... el hilo que se encargue del primer nodo quedará desaprovechado mientras que el que se encargue del segundo tendrá mucho trabajo por delante, por lo que debemos poder reutilizar los hilos de forma dinámica.
Estos dos problemas quedan resueltos con el siguiente modelo:
Nota: Estos ejemplos de código son orientativos y simplificados.
static BrowseTreeDlgt_ BrowseTreeDlgt = new BrowseTreeDlgt_(BrowseTree);
static void BrowseTree(Node root)
{ ProcessNode(root);
foreach (Node n in GetNodeChilds(root))
{ if (runningThreads<maxThreads)
{ runningThreads++;
BrowseTreeDlgt.BeginInvoke(n,
delegate(IAsyncResult ia)
{ BrowseTreeDlgt.EndInvoke(ia);
runningThreads--;
},
null);
}
else
{ BrowseTreeDlgt.Invoke(n);
}
}
}
maxThreads define el número máximo de hilos a usar y runningThreads los que tenemos ya en funcionamiento, si hay hilos disponibles se lanza el nodo en otro hilo (.BeginInvoke) y si no, pues se continua con el hilo actual (.Invoke) como si de una función recursiva se tratase. Cada vez que creamos un hilo, incrementamos runningThreads y lo decrementamos cuando acaba, pero ... ¿en que orden exactamente? Pues el incremento lo antes posible y el decremento lo más tarde posible, de forma que se reduzca la posiblidad de crear hilos de más. De esta forma controlamos la cantidad de hilos.
Este modelo tiene una pega particular, y es el hecho de que cuando lanzamos nodos con todos sus hijos en otro hilo, la ejecución del hilo principal llegará al final y no sabremos si el resto de hilos secundarios han acabado ya de procesar todos sus nodos. El mejor planteamiento es bloquear el hilo que llama a la función hasta que se acabe de recorrer el arbol, para ello bloqueamos dicho hilo y cuando el número de hilos secundarios sea 0, lo liberamos. Sería algo así:
static void BrowseTree(Node root)
{ ProcessNode(root);
foreach (Node n in GetNodeChilds(root))
{ if (runningThreads<maxThreads)
{ runningThreads++;
BrowseTreeDlgt.BeginInvoke(n,
delegate(IAsyncResult ia)
{ BrowseTreeDlgt.EndInvoke(ia);
runningThreads--;
if (runningThreads == 0)
lock (endLock)
Monitor.Pulse(endLock);
},
null);
}
else
{ BrowseTreeDlgt.Invoke(n);
}
}
}
Y al llamar a la función:
lock (endLock)
{ BrowseTree(node);
Monitor.Wait(endLock);
}
Esto hará que se llame a BrowseTree y cuando termine el hilo principal quede bloquedado hasta que runningThreads sea 0. Pero siguen habiendo problemas... :D
Una de las cosas que debemos tener en cuenta en entornos multithreading es que no hay un orden concreto y que la memoria puede ser alterada en cualquier momento, incluso entre instrucción e instrucción. Si por cualquier motivo, el hilo principal acaba después de los hilos secundarios... (por que le ha tocado procesar más nodos..) ... el Pulse llegaría antes que el Wait, con lo que la aplicación quedaría en un deadlock. Para evitar esta situación, lo ideal es llamar a BrowseTree asíncronamente de forma que el hilo que llama a la función quede bloqueado en el Wait inmediatamente y aunque añadamos un hilo adicional... la carga es la misma:
lock (endLock)
{ BrowseTreeDlgt.BeginInvoke(node,
delegate(IAsyncResult ia)
{BrowseTreeDlgt.EndInvoke(ia);}, null);
Monitor.Wait(endLock);
}
Ahora que esta resuelto el tema de "bloquear hasta terminar", hay que dar un repaso a la condición que lo desbloquea, la de que los hilos secundarios en ejecución sea 0... Como decía la memoria puede ser alterada y/ó evaludada en cualquier momento, entonces se puede dar la situación (de hecho se da) de que se haga un Pulse sobre el bloqueo mientras que se esté a punto de iniciar otro hilo secundario porque hay más nodos que procesar en el bucle... Por este motivo, hay que tener en cuenta si hay nodos por procesar antes de hacer el Pulse, es decir, la condición para que se libere el bloqueo y por tanto se de por terminado el recorrido por el arbol es, que no haya hilos en ejecución y que los nodos por procesar sean 0, con lo que se asegura que cuando se den estas dos condiciones es el último hilo en ejecución. Ahora debemos comprobar tanto al final de las ejecuciones síncronas como asíncronas:
static void BrowseTree(Node root)
{ ProcessNode(root);
Node[] childs =GetNodeChilds(root);
nodesRemaining += childs.Length;
foreach (Node n in childs)
{ if (runningThreads < maxThreads)
{ runningThreads++;
BrowseTreeDlgt.BeginInvoke(n,
delegate(IAsyncResult ia)
{ BrowseTreeDlgt.EndInvoke(ia);
runningThreads--;
nodesRemaining--;
if ((runningThreads == 0)&&
(nodesRemaining==0))
lock (endLock)
Monitor.Pulse(endLock);
},
null);
}
else
{ BrowseTreeDlgt.Invoke(n);
nodesRemaining--;
if ((runningThreads == 0) &&
(nodesRemaining == 0))
lock (endLock)
Monitor.Pulse(endLock);
}
}
}
Como handicap el uso de multiples hilos trae los problemas que implica la sincronización, cosa en la que habrá que poner sumo cuidado y ... sobre todo ... imaginación, la principal herramienta del programador. La forma ideal de ejecutar esta función sería algo más compleja:
static BrowseTreeDlgt_ BrowseTreeDlgt = new BrowseTreeDlgt_(BrowseTree);
static void BrowseTree(Node root)
{ ProcessNode(root);
Node[] childs =GetNodeChilds(root);
lock(endLock)
nodesRemaining += childs.Length;
foreach (Node n in childs)
{ if (runningThreads < maxThreads)
{ Interlocked.Increment(ref runningThreads);
BrowseTreeDlgt.BeginInvoke(n,
delegate(IAsyncResult ia)
{ try
{ Monitor.Enter(endLock);
BrowseTreeDlgt.EndInvoke(ia);
runningThreads--;
nodesRemaining--;
}
finally
{ if ((runningThreads == 0) &&
(nodesRemaining == 0))
Monitor.Pulse(endLock);
Monitor.Exit(endLock);
}
},
null);
}
else
{ BrowseTreeDlgt.Invoke(n);
lock (endLock)
{ nodesRemaining--;
if ((runningThreads == 0) &&
(nodesRemaining == 0))
Monitor.Pulse(endLock);
}
}
}
}
Como se puede observar, el principal problema es no saber cuantos nodos tiene el arbol hasta que se han recorrido todos, pero con este planteamiento queda resuelto, al menos es lo que dicen las pruebas concienzudas con distintas CPUs y sistemas operativos que he hecho.
En el próximo artículo un ejemplo de una clase que realiza esta tarea mismo, con ejemplos incluidos ... ;)
Este es un (largo) ejemplo de como crear un cliente FTP asíncrono que gestione múltiples descargas de forma paralela. En este modelo, generalmente la lógica de la aplicación suele estar compuesta por métodos estáticos, pero se añade un objeto de estado (objectState) que realiza el seguimiento de la tarea durante las fases de la aplicación, el estado de la tarea puede notificarse mediante eventos por cambio, temporizados ó devolver el mismo objeto de estado para su seguimiento externo.
En este caso, he optado por devolver el propio objeto de estado pero protegido con una interfaz para que solo determinados miembros puedan ser accesibles y solo para lectura, el final de la tarea se indica con un evento que trae el mismo tipo de objeto. Guardar el objeto de estado y monitorizarlo ó simplemente esperar al evento de fin... queda a la libre elección.
[more]
La interfaz y el delegado que define al evento:
public interface IFtpDownloadInfo
{ Int32 ID{get;} Boolean Ended{get;} Int64 Donwloaded{get;} Exception Error{get;} Int64 ElapsedMilliseconds{get;} Int64 Length{get;} String LocalDirectory{get;} String RemoteFile{get;} String FileName { get;}}
public delegate void FtpEventDlg_(IFtpDownloadInfo e);
La clase que define el objeto de estado cumple esta interfaz, de forma que cuando enviemos el evento, relamente se pasa dicho objeto, pero solo serán accesibles los miembros de dicha interfaz. Esta clase contiene toda la información y campos necesarios para realizar el tracking de la descarga, además implementa el patrón desechable para poder liberar los recursos correctamente una vez haya terminado. El método .ToString ha sido redefinido para poder obtener una información rápida del objeto:
internal class FtpDownloadState:IFtpDownloadInfo,IDisposable
{ private readonly Int32 id;
private Int64 length = 0;
private Int64 downloadedBytes = 0;
private String localDirectory;
private String remoteFile;
private String fileName;
private Exception error;
public Int32 ID{ get { return id; }}
public Boolean Ended { get { return (downloadedBytes == length)&&(length!=0);}}
public Int64 Donwloaded { get { return Interlocked.Read(ref downloadedBytes); } set { Interlocked.Exchange(ref downloadedBytes,value); }}
public Exception Error{ get { return error; } set { error = value;}}
public Int64 ElapsedMilliseconds{ get { lock(DownloadStopWatch) return DownloadStopWatch.ElapsedMilliseconds;}}
public Int64 Length{ get { return length; } set { length = value; }}
public String LocalDirectory{ get { return localDirectory; }}
public String RemoteFile { get { return remoteFile; } }
public String FileName { get { return fileName; } }
private Boolean disposing = false;
internal Stopwatch DownloadStopWatch = new Stopwatch();
internal Int32 BufferLength;
internal NetworkCredential Credential;
internal FtpWebRequest Ftpcon;
internal Stream Target;
internal Stream Source;
internal Byte[] Buffer;
internal FtpDownloadState(Int32 ID, String LocalDirectory, String RemoteFile,
Int32 BufferLength, String Login, String Password)
{ this.localDirectory = LocalDirectory;
this.remoteFile = RemoteFile;
this.BufferLength = BufferLength;
if ((Login!=null)&&(Login.Length>0))
this.Credential = new NetworkCredential(Login, Password);
this.fileName = Path.GetFileName(this.RemoteFile);
this.Buffer = new byte[this.BufferLength];
this.id = ID;
}
public override string ToString()
{ if (this.error == null)
{ return string.Format("{0}: {1}kB/{2}kB en {3}s.", this.fileName, this.Donwloaded / 1024, this.Length / 1024,
this.ElapsedMilliseconds / 1000);
}
else
{ return string.Format("{0}: {1}", this.fileName, this.error.Message); }
}
private void Dispose(Boolean d)
{ if (!disposing)
{ this.Target.Dispose();
this.Source.Dispose();
GC.SuppressFinalize(this);
}
}
public void Dispose()
{ Dispose(true);
}
~FtpDownloadState()
{ Dispose(true);
}
}
Como se puede ver, esta clase no tiene funcionalidad FTP en si misma, lo que permite poder substituir la entidad ó la lógica de forma separada, incluso trabajar con otros tipos de Streams.
Sobre la sincronización, se protege ElapsedMillisecons y Downloaded porque son dos miembros de 64bits que pueden ser modificados al mismo tiempo que se leen, por lo que hay que asegurar la atomicidad.
Ahora la lógica que realiza todo el proceso de la descarga de FTP. El principio de funcionamiento es el mismo que el ejemplo de la descarga FTP, solo que ahora uso .BeginRead en lugar de .Read. Lamentablemente, para obtener cual es la longitud total del archivo hay que conectar en modo GetFileSize antes, lo cual complica un poco la lógica, que aún así es bastante simple:
static public class FtpAsyncDownload
{ // Evento de fin de descarga
static public event FtpEventDlg_ FtpDownloadEvent;
// Orden de descarga
// Prepara el objectState y obtiene pide el tamaño.
static public IFtpDownloadInfo Download(Int32 ID, String LocalDirectory,
String RemoteFile,Int32 BufferLength,
String Login, String Password)
{ FtpDownloadState ftpdwn =
new FtpDownloadState(ID, LocalDirectory, RemoteFile, BufferLength,
Login, Password);
ftpdwn.Ftpcon = (FtpWebRequest)FtpWebRequest.Create(ftpdwn.RemoteFile);
ftpdwn.Ftpcon.Credentials = ftpdwn.Credential;
ftpdwn.Ftpcon.KeepAlive = false;
ftpdwn.Ftpcon.UseBinary = true;
ftpdwn.Ftpcon.Proxy = null;
ftpdwn.Ftpcon.EnableSsl = false;
ftpdwn.Ftpcon.Method = WebRequestMethods.Ftp.GetFileSize;
ftpdwn.Source = ftpdwn.Ftpcon.GetResponse().GetResponseStream();
ftpdwn.Source.BeginRead(ftpdwn.Buffer, 0,ftpdwn.BufferLength,
startDownload, ftpdwn);
return ftpdwn;
}
// Comienza la descarga
// Obtiene el tamaño y pide el inicio de
// la descarga del archivo.
static private void startDownload(IAsyncResult ia)
{ FtpDownloadState ftpdwn = ia.AsyncState as FtpDownloadState;
try
{ ftpdwn.Source.EndRead(ia);
ftpdwn.Length = ftpdwn.Ftpcon.GetResponse().ContentLength;
if (ftpdwn.Length <= 0) throw new ArgumentException("FileSize <=0");
ftpdwn.Ftpcon = (FtpWebRequest)FtpWebRequest.Create(ftpdwn.RemoteFile);
ftpdwn.Ftpcon.Credentials = ftpdwn.Credential;
ftpdwn.Ftpcon.KeepAlive = false;
ftpdwn.Ftpcon.UseBinary = true;
ftpdwn.Ftpcon.Proxy = null;
ftpdwn.Ftpcon.EnableSsl = false;
ftpdwn.Ftpcon.Method = WebRequestMethods.Ftp.DownloadFile;
ftpdwn.Target = new FileStream(Path.Combine(ftpdwn.LocalDirectory, ftpdwn.FileName),
FileMode.Create, FileAccess.Write, FileShare.None);
ftpdwn.Source = ftpdwn.Ftpcon.GetResponse().GetResponseStream();
ftpdwn.DownloadStopWatch.Start();
ftpdwn.Source.BeginRead(ftpdwn.Buffer, 0, ftpdwn.BufferLength,
downloadCallback, ftpdwn);
}
catch (Exception ex)
{ ftpdwn.Error = ex;
if (FtpDownloadEvent != null)
FtpDownloadEvent.Invoke(ftpdwn);
}
}
// Descarga
// Función que se ejecuta cada vez que se obtiene
// un buffer completo para escribir y volver a ejecutarse
// hasta que acabe.
static private void downloadCallback(IAsyncResult ia)
{ FtpDownloadState ftpdwn = ia.AsyncState as FtpDownloadState;
try
{ int readed = ftpdwn.Source.EndRead(ia);
ftpdwn.Target.Write(ftpdwn.Buffer, 0, readed);
ftpdwn.Donwloaded += readed;
if (!ftpdwn.Ended)
{ ftpdwn.Source.BeginRead(ftpdwn.Buffer, 0, ftpdwn.BufferLength,
downloadCallback, ftpdwn);
}
else
{ try
{ if (FtpDownloadEvent != null)
FtpDownloadEvent.Invoke(ftpdwn);
}
catch { }
ftpdwn.DownloadStopWatch.Stop();
ftpdwn.Dispose();
}
}
catch (Exception ex)
{ ftpdwn.Error = ex;
if (FtpDownloadEvent != null)
FtpDownloadEvent.Invoke(ftpdwn);
}
}
}
Básicamente el funcionamiento consiste en lo siguiente:
- Se llama al método Download.
- Este crea un nuevo objeto de estado e invoca asíncronamente la obtención del tamaño del archivo. Indica que al finalizar vaya al método startDownload.
- Se ejecuta startDownload, obtiene el tamaño de archivo, configura la conexión para realizar la descarga e invoca asíncronamente la descarga del archivo. Indica que al finalizar vaya al método downloadCallback.
- Cuando haya descargado un buffer completo, se llama a downloadCallback, escribe el resultado en el archivo y si no se ha descargado todo aún se vuelve a invocar asíncronamente la descarga hasta que complete.
- Cuando acaba, dispara el evento y llama a .Dispose para que libere los Streams del objeto de estado.
Para realizar una descarga, simplemente hay que subscribirse al evento e invocar el método estático Download con los parámetros adecuados:
FtpAsyncDownload.FtpDownloadEvent +=
new FtpEventDlg_(FtpAsyncDownload_FtpDownloadEvent);
FtpAsyncDownload.Download(0, Environment.CurrentDirectory, @"ftp://miftp.com/miarchivo.rar",
8192, "MiLogin", "M1P455W0RD");
Como decía anteriormente, el método Download devuelve un objeto de tipo IFtpDownloadInfo con el que podemos monitorizar el estado de la descarga. Una vez acabe, se disparará el evento si estamos subscritos a él.
Vamos con una de Arts Attack... El ProgressBar de .NET es "bonito", sobre todo en Windows Vista, pero a mi gusto prefería menos destellos fashion y un poco más de información... y ya puestos ... un estilo flat :D
En vez de crear uno from scratch, mucho mejor extender el ProgressBar, añadirle nuevas propiedades y redefinir como repintarlo. Código fuente al final del artículo ;)
Entre los puntos fuertes de extender un control WinForm, esta el de redifinir los estilo con Control.SetStyle y sobreescribir el método OnPaint, dentro de este obtemenos el objeto Graphics con el que podemos pintar formas y textos. Mediante atributos configuramos los elementos que Visual Studio usará para mostrar sus propiedades:
- ToolBoxBitmap: Sobre la clase, indica el icono
que mostrará Visual Studio en la paleta de componentes. Se añade una imagen .bmp al proyecto como recurso embebido, después se indica tal y como aparece en el código. A veces no funciona bien, así que hay un truco creando esa clase vacia 'resfinder' y luego indicando la imagen junto al Namespace, funciona siempre :D - Browsable: Sobre una propiedad, indica si Visual Studio debe mostrarla en el Property Grid.
- CategoryAttribute: Sobre una propiedad, indica en que categoria del Property Grid colocarla.
- DescriptionAttribute: Sobre una propiedad, indica la descripción de esa propiedad en el Property Grid.
Una vez generado el componente en un ensamblado, para usarlo solo tenemos que añadirlo a la paleta de nuestro VS2005, desde el ToolBox, botón derecho, Choose Items, le damos a Browse para ir hasta el ensamblado, lo selecionamos y se añade a la paleta.
Asi mediante VS2005 podemos configurar los nuevos elementos de nuestro ProgressBar, además de ocultar otros que no nos interesan como se puede ver con por ejemplo la propiedad Style.
Pintar en un formulario es relativemente fácil con la clase Graphics y sus utilidades, pero requiere algo de paciencia y "prueba-error" para ajustarlo adecuadamente. Me apunto en el TODO escribir sobre las nociones básicas de GDI+.
Y ahora... el código fuente:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.ComponentModel;
internal class resfinder { }
namespace CustomProgressBars
{ [ToolboxBitmap(typeof(resfinder), "FlatProgressBar.FlatProgressBar.bmp")]
public class FlatProgressBar : ProgressBar
{ private Pen _border;
private String _text;
private SolidBrush _brushForeColor;
private SolidBrush _brushBarColor;
[Browsable(true)]
[CategoryAttribute("FlatProgressBar"), DescriptionAttribute("Color de la Barra")] public Color BarColor
{ get { return _brushBarColor.Color; } set { _brushBarColor.Color = value; } }
[Browsable(true)]
[CategoryAttribute("FlatProgressBar"), DescriptionAttribute("Color del Texto")] public Color TextColor
{ get { return _brushForeColor.Color; } set { _brushForeColor.Color = value; } }
[Browsable(true)]
[CategoryAttribute("FlatProgressBar"), DescriptionAttribute("Color del Borde")] public Color BorderColor
{ get { return _border.Color; } set { _border.Color = value; } }
[Browsable(true)]
[CategoryAttribute("FlatProgressBar"), DescriptionAttribute("Color del Borde")] public Single BorderWidth
{ get { return _border.Width; } set { _border.Width = value; } }
[Browsable(true)]
[CategoryAttribute("FlatProgressBar"), DescriptionAttribute("Color de Fondo")] public new Color BackColor
{ get { return base.BackColor; } set { base.BackColor = value; } }
[Browsable(true)]
[CategoryAttribute("FlatProgressBar"), DescriptionAttribute("Texto de la barra"), DefaultValue("Text")] public new String Text
{ get { return _text; } set { _text = value; } }
[Browsable(true)]
[CategoryAttribute("FlatProgressBar"), DescriptionAttribute("Tipo de fuente del texto.")] public new Font Font
{ get { return base.Font; } set { base.Font = value; } }
[Browsable(false)]
new public ProgressBarStyle Style
{ get { return ProgressBarStyle.Continuous; } set { } }
[Browsable(false)]
new public Int32 MarqueeAnimationSpeed
{ get { return 0; } set { } }
[Browsable(false)]
new public Color ForeColor
{ get { return TextColor; } set { } }
public FlatProgressBar()
{ InitializeComponent();
}
private void InitializeComponent()
{ this.SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer |
ControlStyles.ResizeRedraw |
ControlStyles.SupportsTransparentBackColor, true);
_border = new Pen(Brushes.Black);
_brushForeColor = new SolidBrush(Color.White);
_brushBarColor = new SolidBrush(Color.Gray);
base.Font = new Font("Tahoma", 10, FontStyle.Bold); }
protected override void OnPaint(PaintEventArgs e)
{ // cuando mide cada paso
Single step = (Single)this.Width / this.Maximum;
// cuantos puntos es el progreso actual
Single progressInPoints = (this.Value * step);
progressInPoints = progressInPoints > 0 ? progressInPoints : 0;
// barra de progreso
e.Graphics.FillRectangle(_brushBarColor, 0, 0, progressInPoints, this.Height);
// texto con antialias
SmoothingMode bk = e.Graphics.SmoothingMode;
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
// escribo el texto
e.Graphics.DrawString(_text, Font, _brushForeColor,
new PointF(6, (this.Height - this.Font.Height) / 2));
string percent = ((this.Maximum / 100) * this.Value).ToString() + '%';
percent = percent.PadLeft(4, ' ');
// mido el porcentaje en puntos para saber
// donde ubicarlo
SizeF swidth = e.Graphics.MeasureString(percent, base.Font);
// escribo porcentaje
e.Graphics.DrawString(percent, base.Font, _brushForeColor,
new PointF((this.Width - _border.Width - swidth.Width - 1),
(this.Height - base.Font.Height) / 2));
// restauro el modo
e.Graphics.SmoothingMode = bk;
// dibujo el border si es mayor que 0
if (_border.Width >= 1)
e.Graphics.DrawRectangle(_border, 0, 0, this.Width - 1, this.Height - 1);
}
}
}
En C#, el orden de evaluación de operadores es de izquierda a derecha por orden de prioridad en cualquier tipo de operación. Además, a la hora de evaluar condiciones lógicas se puede anticipar el resultado final si el resultado parcial hasta el momento es inamovible, es decir, que pase lo que pase en el resto de condiciones el resultado parcial será el definitivo, en ese caso, el resto de condiciones no son evaluadas.
Desde el punto de vista del rendimiento, nos permite ahorrar evaluaciones (y con ello instrucciones) innecesarias en nuestra lógica si nos aprovechamos bien de esta característica.
Para ejemplificar este comportamiento, observa este simple programa:
static void Main(string[] args)
{ if (Test1(false) || Test2(false) || Test3(false))
{ Console.WriteLine("Dentro!"); }
Console.ReadKey();
}
static Boolean Test1(Boolean param)
{ Console.WriteLine("Test1"); return param ^ true;
}
static Boolean Test2(Boolean param)
{ Console.WriteLine("Test2"); return param ^ true;
}
static Boolean Test3(Boolean param)
{ Console.WriteLine("Test3"); return param ^ true;
}
}
La instrucción 'if' evalua una condición formada por dos operaciones OR sobre tres métodos que devuelven true ó false. Al devolver 'Test1' un true y siendo una opearción OR sea cual sea el resultado de 'Test2' y 'Test3' ... el resultado final será true, por lo que estos dos últimos no son evaluados. El programa devuelve la siguiente salida:
Test1
Dentro!
Sin embargo, si modificamos la línea del 'if' por:
if (Test1(true) || Test2(false) || Test3(false))
Test1
Test2
Dentro!
El resultado varia, ya que al devolver 'Test1' false, el resultado final no se puede anticipar, pero cuando 'Test2' devuelve true, ya no es necesario evaluar 'Test3':
Por ejemplo si sustituimos los OR por AND, el resultado es distinto ... pero siguiendo la misma línea de actuación:
if (Test1(false) && Test2(false) && Test3(false))
Test1
Test2
Test3
Dentro!
Al ser las dos primeras condiciones true, cualquier resultado parcial podría ser anulado por un false en 'Test3', por lo que debe evaluar las 3ª.
Este comportamiento aplica a cualquier tipo de operación booleana, por compleja que sea, si en un determinado momento dada la ecuación lógica, se sabe que el resultado parcial será definitivo... el resto se obvia.
La última sentencia 'if' probada, podría ser fácilmente substituible por condiciones anidadas ya que al igual que la operación con AND, evaluar una condición implica que se ha superado la anterior:
if (Test1(false))
{ if (Test2(false))
{ if (Test3(false))
{ Console.WriteLine("Dentro!"); }
}
}
Pero en el resto de operaciones implica estructuras más complejas, que al igual que esta última (por simple que sea) deben ser evitadas siempre que no haya que emprender aluguna acción por cada condición a evaluar ó sea un requerimiento evaluarlas todas.
Las estructuras condicionales desmesuradas y mal estructuradas, no solo implican un código ilegible y niveles de identación inaceptables, si no que una mala estructuración puede derivar en la evaluación redundante e innecesaria de condiciones. Evaluar una condición simple como el valor de una variable puede ser algo insignificante, pero si la evaluación es directamente resultado de un método... ejecutar dicho método ó no puede significar una diferencia de rendimiento proporcional al tiempo de CPU que lleva ejecutarlo. Evita la lógica innecesaria !!
Una vez tengamos claro en flujo lógico de nuestra aplicación, y si como decía las condiciones se evaluan pero no se hace nada por cada una de ellas, podemos simplificar fácilmente pequeños grupos de evaluaciones mediante las tablas de Karnaugh, y ecuaciones ya más grandes mediante el algrebra de Boole, muy muy útil para simplificar bloques de lógica de negocios. Dos cosas de las que escribiré en mi blog en breve...
Como colofón, cuando evaluemos condiciones que pueden tomar muchos valores, mejor que la sucesión continua de else if, es mejor usar la instrucción switch. Como muestra un sencillo benchmark (que llevaba tiempo sin hacer uno xD):
Stopwatch sw = new Stopwatch();
sw.Start();
for(int i =0;i<1000000;i++)
TestCharIfElse('z');sw.Stop();
Console.WriteLine("TestCharIfElse: {0} ms.",sw.ElapsedMilliseconds);sw.Reset();
sw.Start();
for (int i = 0; i < 1000000; i++)
TestCharSwitch('z');sw.Stop();
Console.WriteLine("TestCharSwitch: {0} ms.", sw.ElapsedMilliseconds);
Por un lado, 'TestCharIfElse' que com prueba si un caracter está en el alfabeto por medio de instruciones 'if else':
static Boolean TestCharIfElse(Char c)
{ if (c == 'a')
{ return true;
}
else if (c == 'b')
{ return true;
}
[... Omitido por brevedad...]
else if (c == 'z')
{ return true;
}
else return false;
}
Por otro, 'TestCharSwitch', que realiza la misma comprobación pero con la instrucción switch:
static Boolean TestCharSwitch(Char c)
{ switch (c)
{ case 'a': return true;
case 'b': return true;
[... Omitido por brevedad ...]
case 'z': return true;
default: return false;
}
}
La prueba envia un millón de veces el caracter 'z', que obliga a 'TestCharElseIf' a recorrer todas sus condiciones, mientras que 'TestCharSwitch' lo encuentra de forma más rápida como muestra el resultado:
TestCharIfElse: 57 ms.
TestCharSwitch: 12 ms.
Casi 5 veces más rápido en este caso.
Un String, es un tipo de referencia especial llamado inmutable, que quiere decir que el dato almacenado en el heap no se puede cambiar, cuando asignamos un nuevo valor a una variable de este tipo, un nuevo dato es generado en otra posición del heap y su dirección de memoria asignada a dicha variable, el dato anterior queda listo para ser GarbageCollected. Bien, pues este comportamiento que ... a unos les gusta más... a otros menos... se ideó así para evitar condiciones de anticipación en entornos multithreading tanto por motivos de consistencia como de seguridad, y tiene la evidente desventaja del engorro de memoria y trabajo extra para el GC que provoca.
El String Interning es una técnica de optimización que aplica el CLR sobre los String, que consiste en que los literales de este tipo de nuestro dominio de aplicación, son almacenados en un HashTable interno, de forma que si dos variables tienen el mismo valor y este ha sido internado, ambas variables apuntan a la misma referencia. Esto es facilmente comprobable con este simple código:
String s1 = "Darker than BLACK";
String s2 = "Darker than BLACK";
Console.WriteLine(Object.ReferenceEquals(s1, s2)); // True
Ó por ejemplo:
String s1 = "Darker than BLACK";
Console.WriteLine(Object.ReferenceEquals(s1, "Darker than BLACK")); // True
Este proceso de internamiento se lleva a cabo por el JIT de forma dinámica, y como decía, solo sobre los literales, si el String es el resultado de una operación no es internado:
String s1 = "Darker than BLACK";
StringBuilder sb1 = new StringBuilder("Darker than BLACK");
Console.WriteLine(Object.ReferenceEquals(s1, sb1.ToString())); // False
Igual pasa con el resto de operaciones...
String s1 = "Darker than BLACK";
Console.WriteLine(Object.ReferenceEquals(s1, "Darker than"+" BLACK")); // True
Oh Wait! Que sucede aquí? Pues que el CLR como siempre, optimizando al máximo, entiende esa concatenación de literales como un literal tal cual porque inevitablemente va a resultar en dicho dato jejeje, ... un poco más dificil:
String s1 = "Darker than BLACK";
String s2 = String.Empty;
foreach (char c in s1) s2 += c;
Console.WriteLine(Object.ReferenceEquals(s1, s2)); // False
Podemos insertar directamente un String en la tabla de internados mediante el método String.Intern que nos devolverá un String apuntando al dato internado, si no existiese lo crea:
String s1 = "Darker than BLACK";
String s2 = String.Empty;
foreach (char c in s1) s2 += c;
Console.WriteLine(Object.ReferenceEquals(s1, s2)); // False
s2 = String.Intern(s2);
Console.WriteLine(Object.ReferenceEquals(s1, s2)); // True
Cabe destacar, que los String que añadamos a dicha tabla, no pueden ser recolectados por el GC hasta que se descargue el AppDomain, con lo que indica que habrá objetos inmortales en el heap hasta entonces ocupando memoria.
Además, añadir elementos a la tabla de internados también tiene su coste en rendimiento, pero un uso intencionado de esta característica puede mejorar mucho el rendimiento si se aprovecha bien, ya que podriamos realizar comparaciones de Strings con Object.ReferenceEquals en lugar de String.Equals(==), ya que el primero es más rápido al simplemente comparar las referencias, mientras que el segundo primero para saberlo evalua el número de caracteres y si coincide también caracter a caracter, pero claro, esto en el caso de que trabajemos siempre con los mismos String y esten todos internados :P
También podemos simplemente consultar si un String ha sido internado con el método String.IsInterned, que devuelve el String internado en caso de que sí, y nulo en caso de que no:
StringBuilder sb1 = new StringBuilder();
sb1.AppendFormat("{0} is {1}", "Hei", "BK201");
Console.WriteLine(String.IsInterned(sb1.ToString())==null); // True
Console.WriteLine(String.IsInterned("BK201") == null); // False
En el CLR 2.0, este comportamiento es por defecto, y aunque existe el atributo CompilationRelaxations que permitiria anularlo ... el CLR se caga en él lo ignora. En cualqueir caso no se debe desarrollar dando por hecho este comportamiento ya que en futuras versiones del CLR podría cambiar.
Ventaja? Desventaja? Verdaderamente es algo engorroso. Por un lado se suele recomendar no trabajar nunca con String harcodeados (literales) en nuestro código, y por otro siempre que se trabaja con Strings se suele realizar operaciones con ellas por lo que es dificil asegurar que un String a evaluar este internado. Aunque es cierto que como comentaba antes podamos sacar partido a esta funcionalidad en algunas ocasiones contadas, por ejemplo si obtenemos de una BD una serie de cadenas de texto que no van a ser alteradas durante la ejecución y solo se utilizan como referencia de comparación ... podemos sacar partido a Object.ReferenceEquals para su evaluación.
Y después de este largo post, que no tiene más utilidad que ser una curiosidad insana del CLR, aún te quieres devanar los sesos un poco más ... realmente ... ¿el método String.IsInterned funciona bien?
String s1 = "Black Shinigami";
StringBuilder sb = new StringBuilder("Black Shinigami");String s2 = sb.ToString();
Console.WriteLine(String.IsInterned(s1) != null); // True
Console.WriteLine(String.IsInterned(s2) != null); // True
Console.WriteLine(Object.ReferenceEquals(s1, s2)); // False
Según este código, el CLR entiende que s1 y s2 estan internados, pero realmente solo lo esta s1, aunque s2 contiene un dato internado ... la posición del heap a la que apunta dicha variable no es la que esta internada ... ¿que sucede? Pues ya que en los foros del MSDN no saben no entienden mi inglés xD ... la solución me la ha dado el fabuloso libro CLR via C#, donde se explica que el HashTable de las String internadas, usa el dato como key y la dirección de memoria del dato internado como valor, lo que explica que el CLR busca el dato contenido en s2 como clave en el HashTable y obtiene la dirección de memoria en el heap de s1, que es la que devuelve exactamente ... sabiendo esto podemos usar un método mejor:
static Boolean IsThisInterned(String str)
{ return Object.ReferenceEquals(String.IsInterned(str), str);
}
Ya finalizando, aclarar que el String Pooling es una técnica similar pero distinta, que aplica el compilador a las Strings cuando genera los metadatos, de forma que si hay varias Strings literales con el mismo dato en el código fuetne, se introduce una sola vez en los metadatos y todas apuntan a esta. De esta forma se reduce el tamaño final del ensamblado. Esto es algo que ya hacían de forma parecida los compiladores de C/C++.
Como nota final diré que si, que me encanta de Darker Than BLACK :D, de hecho me estoy descargando ahora mismo el último episodio, algo triste pero impacientemente esperado a la vez.
Hasta otro capítulo de cosas que valen para poco.