sábado, 30 de enero de 2010

Programación asíncrona

Hoy vamos a hacer un post un poco más técnico, que ya hace tiempo que no hacemos ninguno. Vamos a hablar, en la línea de este otro post, de la programación asíncrona.

Quién no se ha encontrado alguna vez con la necesidad de hacer una tarea que consume mucho tiempo ( acceso a ficheros, cálculos complejos, acceso a la red, etc ) y no quiere que su interfície de usuario se vea afectado por ello? No veo ninguna mano alzada, así que supondremos que es algo recurrente.

Este tipo de programación se ha llevado a cabo históricamente mediante hilos. Aunque .Net ha facilitado mucho la programación con ellos, hacer un uso de los mismos no deja de ser añadir complejidad al programa y es una fuente típica de errores. Es por esto que existen otras alternativas a la programación asíncrona sin tener que utilizar explícitamente hilos. Estas alternativas son las siguientes:
  • Llamadas asíncronas a delegados.
  • Llamadas asíncronas utilizando la interfaz IAsyncResult.
  • El componente BackgroundWorker.
  • Llamadas asíncronas basadas en eventos.
En este artículo nos centraremos en esta última aproximación ya que es la más completa y la más recomendada.

Este patrón se apoya en la utilización de dos clases de ayuda proporcionadas por .Net: la clase AsyncOperationManager que utilizaremos para crear instancias de la otra clase de ayuda, la AsyncOperation que utilizaremos para efectuar el seguimiento del progreso de la tarea asíncrona e informar del mismo. Una de las ventajas que tiene es que los eventos de progreso y cancelación se hace en el hilo adecuado, con lo que podemos actualizar la interfíce de usuario directamente, sin tener que recurrir a Invoke.

Vamos a ver paso a paso como crear una clase que implemente este patrón y como crear una clase cliente que la instancie y utilice.

Lo primero que tenemos que hacer es, obviamente, crear nuestra clase que queremos que se pueda llamar de manera asíncrona. En nuestro caso creamos una clase llamada LazyClass.

Seguidamente vamos a declarar los delegados y los eventos públicos que necesitamos para informar al cliente del progreso y terminación de la tarea asíncrona.


public delegate void LazyTaskProgressChangedEventHandler(LazyTaskProgressChangedEventArgs pArgs);
public delegate void LazyTaskCompletedEventHandler(object pSender, LazyTaskCompletedEventArgs pArgs);

Esto nos obliga a definir los argumentos LazyTaskCompletedEventArgs y LazyTaskProgressChangedEventArgs.

La clase LazyTaskCompletedEventArgs tendrá la definición de las cosas que queremos que se informen al exterior cuando se haya terminado la tarea asíncrona. Esta clase puede ser tan compleja como queramos, aunque nosotros, por el bien del entendimiento del ejemplo, la hemos reducido lo máximo posible.

public class LazyTaskCompletedEventArgs : AsyncCompletedEventArgs
{
    private int m_iNumIterations = int.MinValue;
    private int m_iTotalTime = int.MinValue;

    ///
    /// Constructor
    ///
    public LazyTaskCompletedEventArgs(int pNumIterations, int pTotalTime, Exception pException, bool pCancelled, object pState) : base (pException, pCancelled, pState )
    {
        m_iNumIterations = pNumIterations;
        m_iTotalTime = pTotalTime;
    }

    ///
    /// Número de iteraciones
    ///
    public int NumIterations
    {
       get
       {
           RaiseExceptionIfNecessary();
           return m_iNumIterations;
       }
    }

    ///
    /// Tiempo transcurrido
    ///
    public int TotalTime
    {
        get
        {
            RaiseExceptionIfNecessary();
            return m_iTotalTime;
         }
    }
}

Como se puede ver, en cada una de las propiedades antes de devolver el valor se llama a una función ( del espacio de nombres System.ComponentModel ) llamada RaiseExceptionIfNecessary. Esta función se utiliza para evitar que alguien acceda a los valores de la clase si se ha producido algún error o se ha cancelado la invocación asíncrona. En tal caso, al acceder a cualquiera de las propiedades de la clase recibiremos una excepción del tipo InvalidOperationException.

La clase LazyTaskProgressChangedEventArgs derivará de la clase ProgressChangedEventArgs del espacio de nombres System.ComponentModel. En nuestro caso no le añadimos nada ( con el porcentaje tenemos suficiente ) pero se le podría añadir todo lo necesario para nuestra tarea en concreto.





///
/// Classe que define los argumentos del progreso de la tarea
///
public class LazyTaskProgressChangedEventArgs : ProgressChangedEventArgs
{
    public LazyTaskProgressChangedEventArgs(int pPercentatge, object pState)
: base(pPercentatge, pState)
    {
    }
}

Lo siguiente es implementar los delegados privados que utilizaremos como puente para informar al cliente del progreso de la tarea. Estos delegados tendrán una firma del tipo SendOrPostCallback, definida también en el espacio de nombres System.ComponentModel.

private SendOrPostCallback onProgressDelegate;
private SendOrPostCallback onCompletedDelegate;

Y los inicializamos en el constructor de la clase:

onProgressDelegate = new SendOrPostCallback(LazyTaskProgress);
onCompletedDelegate = new SendOrPostCallback(LazyTaskCompleted);

A continuación vamos a declarar el delegado encargado de realizar el trabajo que queramos hacer en nuestra clase. Este delegado tendrá que recibir un parámetro del tipo AsyncOperation que se utilizará para hacer el seguimiento del progreso de la tarea asíncrona y un delegado de tipo SendOrPostCallback que se utilizará para notificar del fin de la operación. También puede recibir cualquier otro parámetro que necesitemos para la ejecución de la tarea.

private delegate void WorkerEventHandler ( AsyncOperation pOperation, SendOrPostCallback pCompletionMethod );

Después declararemos una colección para administrar las diferentes operaciones asíncronas que se produzcan en nuestra clase. Podemos utilizar un Dictionary, un HibrydDictionary o lo que más nos convenga.

ListDictionary m_ldTasks = new ListDictionary();

A continuación pasamos a implementar los eventos públicos que utilizaremos para informar al cliente del progreso y terminación de la tarea asíncrona.

///
/// Implementación del delegado del progreso
///
private void LazyTaskProgress(object pSate)
{
    ProgressChangedEventArgs e = pSate as LazyTaskProgressChangedEventArgs;
    FireOnLazyTaskProgressChanged(e);
}

///
/// Implementación del delegado de tarea completada
///
private void LazyTaskCompleted(object pSate)
{
    LazyTaskCompletedEventArgs args = pSate as LazyTaskCompletedEventArgs;
    FireOnLazyTaskCompleted(args);
}

///
/// Lanza el evento de tarea completada
///
private void FireOnLazyTaskCompleted(LazyTaskCompletedEventArgs pArgs)
{
    if (OnLazyTaskCompleted != null)
    {
        OnLazyTaskCompleted(this, pArgs);
    }
}

///
/// Lanza el evento de progreso en la tarea
///
private void FireOnLazyTaskProgressChanged(LazyTaskProgressChangedEventArgs e)
{
    if (OnLazyTaskProgressChanged != null)
    {
        OnLazyTaskProgressChanged(e);
    }
}

Y para acabar con la implementación de delegados tan sólo nos falta implementar el delegado de finalización de la tarea. Este delegado se llamará  cuando se haya terminado la ejecución de nuestra tarea. En este método es donde se elimina el identificador de la tarea de la colección y donde se da por terminada la tarea asíncrona llamando al método PostOperationCompleted. Una vez llamado este método, cualquier intento de utilización de la AsyncOperation a la que se refiere nos dará una excepción.


/// Método que se llamará cuando la ejecución de la tarea de forma asíncrona haya acabado
///
private void LazyTaskCompletionMethod(object pLazyTaskState)
{
    LazyTaskState taskState = pLazyTaskState as LazyTaskState;

    AsyncOperation operation = taskState.Operation;
    int iNumIterations = taskState.NumIterations;
    int iTotalTime = taskState.TotalTime;
    bool bCancelled = false;
    Exception eException = taskState.TaskException;

    LazyTaskCompletedEventArgs args = new LazyTaskCompletedEventArgs(iNumIterations, iTotalTime, eException, bCancelled, operation.UserSuppliedState);

    lock (m_ldTasks.SyncRoot)
    {
        if (m_ldTasks.Contains(operation.UserSuppliedState))
        {
            m_ldTasks.Remove(operation.UserSuppliedState);
        }
    }
    operation.PostOperationCompleted(onCompletedDelegate, args);
}


Ahora ya podemos pasar a implementar la función que hará el trabajo real de nuestra clase. En nuestro caso será algo tan trivial como un for con un sleep dentro para simular una tarea pesada.

///
/// Realiza la operación que queremos
///
private void DoTask(AsyncOperation pOperation, SendOrPostCallback pCompletionMethod)
{
    Exception ex = null;

    try
    {
        for (int i = 0; i < 100; i++)
        {
            if (!TaskCanceled(pOperation.UserSuppliedState))
            {
                System.Threading.Thread.Sleep(100);
                LazyTaskProgressChangedEventArgs pChangedEventArgs = new LazyTaskProgressChangedEventArgs(i + 1, pOperation.UserSuppliedState);
                pOperation.Post(this.onProgressDelegate, pChangedEventArgs);
            }
        }
    }
    catch ( Exception e )
    {
        ex = e;
    }

    if (!TaskCanceled(pOperation.UserSuppliedState))
    {
        LazyTaskState state = new LazyTaskState(100, 10000, pOperation, ex);
pCompletionMethod(state);
    }
}

En este código podemos ver tres cosas. La primera es que nos apoyamos en una función llamada TaskCancelled. Esta función, que la debemos implementar nosotros, simplemente mira en la colección si está el identificador de tarea que le pasamos como parámetro. Por otra parte podemos ver que cada vez que queremos informar de un cambio en el progreso de la tarea llamamos a la función Post de la AsyncOperation que recibimos como parámetro. Y finalmente vemos que cuando acabamos la realización de la tarea llamamos al delegado del completionMethod.

Y ya nos vamos acercando al final. Ahora tan sólo nos queda implementar los métodos para iniciar y cancelar el trabajo. En el método para iniciar la tarea nos tendremos que asegurar que el identificador de tarea que nos pasan cómo parámetro es único mirando si está en la colección. En el método para cancelar, tan sólo tendremos que quitar la tarea de la colección. El framework hará el resto.

///
/// Realiza la tarea asíncronamente
///
public virtual void DoLazyTaskAsync(object pTaskID)
{
    AsyncOperation operation = AsyncOperationManager.CreateOperation(pTaskID);

    lock (m_ldTasks.SyncRoot)
    {
        if (m_ldTasks.Contains(pTaskID))
        {
            throw new ArgumentException("Ya existe una tarea con este identificador", "taskID");
        }
        m_ldTasks[pTaskID] = operation;
    }

    doTaskDelegate = new WorkerEventHandler(DoTask);
    doTaskDelegate.BeginInvoke(operation, completionMethodDelgate, null, null);
}

///
/// Cancela la ejecución de una tarea asíncrona
///
public void CancelLazyTaskAsync(object pTaskId)
{
    lock (m_ldTasks.SyncRoot)
    {
        if ( m_ldTasks.Contains(pTaskId ) )
        {
            AsyncOperation operation = (AsyncOperation)m_ldTasks[pTaskId];
            if (operation != null)
            {
                 m_ldTasks.Remove(pTaskId);
            } 
        }
    }
}

Ya tenemos nuestra clase con una llamada a un método asíncrono preparada para ser utilizada por un cliente. Para completar su funcionalidad deberíamos tener también un método para que podamos llamarla de forma síncrona por si nos interesa.

La implementación del cliente es muy sencilla. Simplemente nos tenemos que subscribir a los eventos de progreso y finalización y llamar a los métodos de inicio y cancelación cuando nos interese. Podéis ver el código completo en el adjunto.

Espero que este pequeño ejemplo os haya servido de ayuda. Podéis descargaros el ejemplo de aquí.

domingo, 17 de enero de 2010

Programación en pareja

Esta es más o menos la cara que se le queda a un gestor de proyectos tradicional cuando le comentas que quieres implantar que el equipo trabaje en parejas. Sin embargo es una de las mejores decisiones que hemos tomado para mejorar la calidad de nuestro código. Que ventajas le vemos? Pues las siguientes serian las principales:
  • Calidad del análisis previo: no sólo programamos en pareja, sino que también hacemos el análisis técnico de la solución en pareja. Esto hace que el diseño sea mucho mejor ya que el error que uno sólo no detectaría, con la ayuda del otro si que lo hace.
  • Calidad del código: el código que sale de la programación en parejas es de mucha más calidad que el hecho por uno sólo. La idea que no tiene uno, la tiene el otro. El comentario que a uno le da pereza añadir, el otro se lo recuerda. El test que uno no cree necesario hacer, el otro le obliga a hacerlo. Y así una larga lista.
  • Concentración: si uno trabaja sólo y tiene ciertos problemas de concentración es posible que no dedique todo el tiempo que seria deseable a programar. Messenger, mail, charlas con el compañero son pequeñas cosas que, si no se gestionan correctamente, pueden hacer perder mucho tiempo a un programador. Trabajando en pareja esto se minimiza muchísimo. Nadie se atreve a charlar por el messenger si está con el compañero trabajando. El focus se aumenta muchísimo.
  • Aprendizaje: siempre se aprenden cosas de los compañeros, a no ser que haya demasiada diferencia entre las capacidades de ambos, con lo cual sólo aprendería uno. Pero si la pareja está bien montada ambos aprenden el uno del otro.
  • Propiedad colectiva del código: al ir cambiando de pareja y de tarea el conocimiento de las características del código va fluyendo entre los integrantes del equipo, haciendo mucho más fácil que alguien se incorpore al equipo.
  • Ánimo del equipo: en general a la gente le gusta más trabajar en equipo que no sólo y el hacerlo le anima a seguir trabajando y cohesiona al grupo.
Así que espero que a partir de ahora os animéis a probar la programación en pareja!