|
|
-
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 :)
Crossposting desde ElBruno.com
|
-
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 ... ;)
Crossposting desde ElBruno.com
|
-
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.
Crossposting desde ElBruno.com
|
-
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); } } }
Crossposting desde ElBruno.com
|
-
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.
Crossposting desde ElBruno.com
|
-
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.
Crossposting desde ElBruno.com
|
-
Gran noticia que me llega a través del correo de Avanade, Releasing the source code for the .NET Framework Libraries :D
Iran incluidas en la salida de .NET 3.5 y VS2008, aunque es posible que esten disponibles para descarga en breve. Se publican bajo la licencia opensource de Microsoft MS-RL.
Se comenzará con la publicación de:
-
NET Base Class Libraries
-
System
-
System.IO
-
System.Collections
-
System.Configuration
-
System.Threading
-
System.Net
-
System.Security
-
System.Runtime
-
System.Text
-
ASP.NET
-
Windows Forms
-
ADO.NET
-
XML
-
WPF
En el artículo se ven instrucciones para integrar el depurado con VS2008 de forma que se pueda seguir la pila de llamadas a través de estas fuentes.
"Tener acceso al código fuente e integración con el depurador de las librerias del .NET Framework será realmente valioso para los desarrolladores en .NET. Ser capaz de avanzar instrucción a instrucción y revisar el código fuente proporciona una mejor idea de como las librerias están implementadas y permite a los desarrolladores construir mejores aplicaciones e incluso hacer un mejor uso de dichas librerias."
Más info también en el blog de Shawn Burke.
Se publicarán las fuentes de la BCL de .NET | vtortola.NET
Crossposting desde ElBruno.com
|
-
Cosilla curiosa que nunca había hecho antes, medir la velocidad de lectura de un Stream, por ejemplo para saber a que velocidad en Bytes/s descargamos de un NetworkStream, ó de un FtpDataStream, ó para saber como de rápido lee nuestro disco duro ... ya sea con fines meramente estadísticos ó para encontrar el tamaño de buffer adecuado ... Es un procedmiento muy sencillo, se trata de dividir los bytes descargados entre los milisegundos transcurridos (cuantos bytes por milisegundo) y multiplicarlo por 1000 (un segundo). Para ello nos apoyamos en la clase para realizar mediciones de tiempo con precisión, StopWatch. En este ejemplo de una descarga FTP, tenemos un FtpDataStream de donde leemos los datos y un FileStream donde los guardamos. Se podría hacer con practicamente cualquier Stream. Adicionalmente declaramos una variable global a la clase Int64 donde almacenar la cantidad de Bytes leidos y un StopWatch global también para poder consultarlos desde otros métodos: private Stopwatch downloadStopWatch = new Stopwatch(); private Int64 downloadedBytes=0;
Cuando vamos a iniciar la lectura, iniciamos el StopWatch y vamos incrementando los bytes leídos:
using (FileStream fs = new FileStream(this.localDirectory, FileMode.Create,FileAccess.Write, FileShare.None)) using (Stream strm = ftp.GetResponse().GetResponseStream()) { // Inicio el cronómetro downloadStopWatch.Start(); contentLen = strm.Read(buff, 0, this.bufferLength); // Incremento los bytes leidos. downloadedBytes += contentLen; while (contentLen != 0) { fs.Write(buff, 0, contentLen); contentLen = strm.Read(buff, 0, this.bufferLength); // Incremento los bytes leidos. downloadedBytes += contentLen; } // Paro el cronómetro. downloadStopWatch.Stop(); }
Cuando queramos saber la velocidad, por ejemplo en KBytes/s podemos invocar a un metodo/propiedad-get tal que así:
public Single DownloadSpeed { get { Double bytesPerSecond; try { bytesPerSecond = (this.downloadedBytes / downloadStopWatch.ElapsedMilliseconds) * 1000; } catch (DivideByZeroException) { // El StopWatch marcaba 0 milisegundos. bytesPerSecond = this.downloadedBytes; } return (Single)(bytesPerSecond / 1024); } }
Nota: Ojo, se puede dar el caso de que consultemos la velocidad y la descarga no se haya iniciado aún ... ó que ni siquiera haya pasado 1ms ... por lo que debemos controlar la división por cero y devolver en dicho caso los bytes transferidos hasta el momento, que serán 0 si la descarga no ha comenzado y X si ha comenzado ya :D ... Yo siempre me pongo en el peor de los supuestos por si acaso !!!! xD
Crossposting desde ElBruno.com
|
-
Antes de empezar a desarrollar una aplicación, es conveniente revisar el diseño lógico para asegurar que es esta completo y correcto. Esto viene a ser evaluar el diseño en los siguientes terminos: - Evaluación de ejecución:
- Rendimiento.
- Escalabilidad.
- Disponibilidad.
- Recuperación.
- Seguridad.
- Evaluación de la arquitectura:
- Mantenibilidad.
- Extensibilidad.
- Evaluación de los requerimientos:
La evaluación del rendimiento determina la velocidad de ejecución de la aplicación. Desde el diseño lógico, tiene dos puntos fundamentales: - Las capas: Asegurar que no se ha dividido en demasiadas capas. Normalmente 3 son suficientes, aunque puede necesitarse más si existen motivos razonables.
- Los niveles de abstracción: Hay que evitar niveles de abstracción innecesarios en las clases. Un abuso podría producir una perdida de rendimiento al forzar al flujo de ejecución a pasar por demasiados objetos.
A nivel de diseño lógico, evaluar el rendimiento queda limitado a la búsqueda y supresión de redundancias. La evaluación de la escalabilidad determina la capacidad de la aplicación para adaptarse al aumento de carga y/ó número de usuarios. Es importante asegurar que dispone de una ó varias capas intermedias separadas, de forma que dichas capas puedan ser puestas en otras máquinas, como por ejemplo en un servidor de gran capacidad mediante Remoting. La evaluación de la disponibilidad y recuperación determina la capacidad de la aplicación para recuperarse y tomar contramedidas en situaciones catastróficas. Es importante asegurar que las clases pueden: - Usar transacciones fiables, ya sean de base de datos, MSMQ, System.EnterpriseServices, DTC, ...etc... De forma que en caso de falla no quede una operación colgada a mitad de realizarse.
- Reconstruir archivos de datos y configuración corruptos por un fallo de continuidad en el hardware, como una perdida de alimentación.
- Tomar fuentes de datos ó servicios redundantes en caso de perdidas de conectividad ó fallos del hardware remoto.
La evaluación de la seguridad determina la capacidad de la aplicación para proteger "sus secretos" (contraseñas, cadenas de conexión, datos sensibles, ..etc..). Los pilares fundamentales son: - Autenticación: Solicitar que el usuario se autentique para poder usar la aplicación, por ejemplo a través de 'Active Directory' ó un sistema propio de contrastar usuarios/contraseñas. Es importante que si se usa un sistema propio, no se almacenen las contraseñas si no sus Hash.
- Cifrado: Asegurar que los datos sensibles que necesariamente se han de guardar en disco, solo puedan ser usados por el usuario que los creo. Por ejemplo, usando la DPAPI. Se debe reducir al máximo el guardado de archivos temporales con datos sensibles. Crear un almacenamiento aislado es una buena opción para ello.
- SQL-Injections: Asegurar que desde ningún formulario de la aplicación ó punto visible se puede realizar una injección de código SQL. La mejor forma de evitar esto es usando parámetros para construir las sentencias SQL.
- Auditoria: Se debe auditar los procesos de autenticación y autorización, de forma que podamos comprobar si hay usuarios intentando acceder a datos protegidos ó intentando autenticarse por fuerza bruta.
- Firmar los ensamblados con nombres seguros para que solo el autor original de la aplicación pueda substituirlos.
La evaluación de la mantenibilidad determina la facilidad con la que la aplicación podrá se mantenida una vez puesta en producción. Siguiendo un diseño en capas y modular se asegura que los cambios podrán realizarse substituyendo piezas de la aplicación y no la aplicación entera. La evaluación de la extensibilidad determina si el código de la aplicación es rehusable ó si se puede implementar con soluciones ya hechas, bien en el .NET Framework ó de nuestra propia empresa. Usando código ya existente aseguramos que utilizamos cosas ya probadas y ahorramos costes de desarrollo. La evaluación de la integridad de los datos determina dos cosas: - La capacidad de la aplicación para mantener los datos acorde al esquema de la fuente, de forma que se respeten resticiones, tipos y contratos, asegurando así que la fuente no rechazará los datos cuando le sean enviados.
- La capacidad de la aplicación para lidiar con la concurrencia de los datos. Existen dos tipos de concurrencia:
- Optimista: Los datos no se bloquean mientras estan siendo leidos por otro usuario. Mejor rendimiento pero se tiene el riesgo de que los datos cambien mientras están siendo leidos, y podrían tomarse decisiones erróneas.
- Pesimista: Los datos se bloquean mientras están siendo leidos por otro usuario. Peor rendimiento pero asegura que cuando un usuario lee un dato, ese dato no cambiará hasta que termine de trabajar con él.
La evaluación de los casos de uso determina si la aplicación sigue cumpliendo con los requerimientos. Se intenta buscar inconsistencias ó ambiguedades en el diseño lógico. Resumen de la lección 3.1 del libro oficial de Microsoft para obtener el MCPD:WinApps.
Crossposting desde ElBruno.com
|
-
Siguiendo con el tema del modelado del software en capas, es el turno de crear modelos físicos más precisos para los programadores que han de escribir el código. Los modelos físicos indican como los programadores deberían desarrollar la aplicación. Un diagrama de componentes indica los componentes ó paquetes de los que se compondrá la aplicación y las dependencias de estos. Un componente es un conjunto de clases relacionadas formando una unidad distribuible, como una DLL, un control ó un Webservice. Los componentes se representan por rectangulos con dos pequeños rectangulos a su izquierda, en el nombre se indica la capa a la que pertenecen separados con dos dobles puntos ('::'). Otros rectangulos mayores que agrupan varios componentes simbolizan un contenedor de distribución llamado nodo, que suele indicar una separación física, como estar en distinto hardware. Los diagramas de clases definen las estructura de las clases y las relaciones entre ellas desde el punto de vista de la programación orientada a objetos, sin especificar como interactuan entre ellas, solo un modelo estático. Este modelo que es independiente de la tecnología y lenguaje usado, representa las clases, sus propiedades, métodos, interfaces, herencia, asociaciones, tipos de datos y demás. Las clases deben derivar de la arquitectura propuesta para la solución. Los diagramas de secuencia muestran como los objetos intearactuan entre ellos durante la ejecución, muestra el tiempo de vida de dicho objetos y los mensajes intercambiados entre ellos. Los objetos se representan como rectangulos en lo alto del diagrama con lineas verticales donde se representa su tiempo de vida y horizontales representando el envio de mensajes a otros objetos. Un diagrama de colaboración muestra el mismo tipo de interacción que el anterior solo que permite distribuirlo todo de una forma más libre, indicando el orden de los mensajes mediante una numeración. Un diagrama de actividad ó flujo representa el flujo de ejecución y permite mostrar actividades que suceden de forma paralela (ó multithreading). Se usan para representar algorritmos más complejos. El pseudo-código no es un diagrama, ni es un lenguaje específico, es una forma de representar el código que ha de realizar el programador de una forma representativa. Es una forma rápida y fácil para los arquitectos y analistas de expresar conceptos a los programadores. Es como cada uno quiera hacerlo, ya sea por facilidad para si mismo ó por facilidad para quien tiene que interprestarlo. Resumen de la lección 2.3 del libro oficial de Microsoft para obtener el MCPD:WinApps.
Crossposting desde ElBruno.com
|
-
Siguiendo con el tema del DbFactory con las clases de System.Data.Common, que permiten trabajar con cualquier base de datos de la que tengamos un proveedor en .NET, explicaré la clase DbTransaction, el cual permite realizar transacciones en la base de datos (explicación más completa en la versión en inglés). Una transacción es una operación formada por un grupo de operaciones relacionadas, cada una podría requerir varias operaciones de lectura y/ó escritura en la base de datos, y solo tiene validez si se ejecutan todas. En caso de producirse un error en el transcurso de la transacción, ha de ser posible “deshacer” todas las operaciones anteriores (rollback), en caso contrario se da por finalizada la transacción aplicando los cambios (commit). Por ejemplo, en un traspaso de dinero entre dos cuentas del mismo banco, la operación incluye primero la substracción del dinero de la cuenta A y su ingreso en la cuenta B, imaginemos que la substracción se realiza correctamente pero hay un error a la hora de ingresar la cantidad en B, en este caso el dinero no quedaría ni en una cuenta ni en otra y podría hasta “perderse”; si esta operación se ejecuta como una transacción, al fallar el ingreso en B, automáticamente se revertiría la substracción en A, con lo que ambas cuentas volverían a quedar en el mismo estado que antes de ejecutarse dicha transacción. Siguiendo con el ejemplo anterior en DbFactory y volviendo a usar la misma base de datos de ejemplo Northwind que viene incluida en SQL Server 2005 Express, en este ejemplo se realizará la inserción de 5 nuevos empleados como una transacción, si todo funciona correctamente se realizará un DbTransaction.Commit() que finalizaría la transacción guardando los datos, mientras que si en el transcurso de la operación se produjese alguna excepción, se ejecutaría DbTransaction.Rollback() devolviendo a la tabla a su estado original. A partir de este código probad a introducir errores en las sentencias SQL para ver como al saltar una DbException se realiza un rollback. string provider = ConfigurationManager.ConnectionStrings["SQLServer"].ProviderName; string connectionString = ConfigurationManager.ConnectionStrings["SQLServer"].ConnectionString; // Sentencia SQL string insertEmployee = "INSERT INTO EMPLOYEES (EmployeeID, LastName, FirstName) VALUES ({0}, ‘{1}’, ‘{2}’)";// Obtengo el proovedor. DbProviderFactory dbpf = DbProviderFactories.GetFactory(provider); // Creo la conexión y el comando. using (DbConnection dbcon = dbpf.CreateConnection()) using (DbCommand dbcmd = dbcon.CreateCommand()) {// Configuro la conexión dbcon.ConnectionString = connectionString; try { // Abro la conexión dbcon.Open(); // Inicio la transación dbcmd.Transaction = dbcon.BeginTransaction(); // Si os falla al insertar filas probad esto: dbcmd.CommandText = "SET IDENTITY_INSERT Employees ON"; dbcmd.ExecuteNonQuery(); //// // Inserto a los nuevos empleados dbcmd.CommandText = string.Format(insertEmployee, 10, "Simpson", "Homer"); dbcmd.ExecuteNonQuery(); dbcmd.CommandText = string.Format(insertEmployee, 11, "Szyslak", "Moe"); dbcmd.ExecuteNonQuery(); dbcmd.CommandText = string.Format(insertEmployee, 12, "Carlson", "Karl"); dbcmd.ExecuteNonQuery(); dbcmd.CommandText = string.Format(insertEmployee, 13, "Leonard", "Lenny"); dbcmd.ExecuteNonQuery(); dbcmd.CommandText = string.Format(insertEmployee, 14, "Gamble", "Barney"); dbcmd.ExecuteNonQuery(); // No ha saltado ninguna excepción, // todo ha ido bien, hago commit y salvo la operación. dbcmd.Transaction.Commit(); Console.WriteLine("Transacción OK!");} catch (DbException dbex) { // Algo salío mal, deshago la transacci | |