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!

lunes, 11 de enero de 2010

Planificando entregas con Scrum (I)


En el post que nos ocupa vamos a ver los pasos para realizar la planificación de las diferentes entregas o releases de un proyecto, utilizando Scrum (aunque este proceso podría fácilmente aplicarse a cualquier metodología iterativa).

Lo que se quiere conseguir es crear una planificación a mas largo plazo, de mas duración que una iteración y que abarque las diferentes entregas del proyecto (que pueden ser varias si se realiza por fases o puede ser una única entrega), y que permita definir una previsión de que funcionalidades se podrán tener finalizadas en una determinada fecha.

En su estado más básico, una planificación de entrega o de proyecto debería contar con como mínimo la siguiente información:

  • Una lista de historias de usuario, a ser posible estimadas y priorizadas (product backlog)
  • Los miembros del equipo que participan en el proyecto (desarrolladores, analistas, administradores de bbdd...)
  • Fecha de inicio de la primera iteración y fecha de fin de la última iteración.
Es importante indicar que este nivel de planificación no se debe caer en el error de intentar entrar en el detalle de que personas van a realizar que tareas, en que secuencia se van a realizar las mismas etc... Estos detalles deben dejarse para la planificación de cada iteración, momento en el que las historias de usuario se dividen en tareas a nivel técnico.

Los pasos generales para realizar la planificación de las entregas son los siguientes:

  • Determinar los criterios mas relevantes para el éxito del proyecto, por ejemplo entregar antes de una fecha determinada, tener un conjunto mínimo de funcionalidades pero una fecha flexible, una combinación de ambas...
  • Realizar la estimación de las historias de usuario, concretamente de aquellas que tienen una alta probabilidad de formar parte de la siguiente entrega. Este proceso se realiza entre el Product Owner y los miembros del equipo (¡¡Solo hace estimaciones el equipo!!) y se estima en puntos de historia o días ideales (y usando alguna técnica estilo Planning Poker)
  • Seleccionar la duración de las iteraciones, en base a la duración del proyecto. Lo normal es de 2 a 4 semanas, pero en proyectos de larga duración se pueden realizar iteraciones mas largas (aunque yo no lo recomiendo) o en proyectos muy cortos se pueden llegar a hacer iteraciones de una única semana.
  • Realizar una estimación de la velocidad, en puntos de historia (o días ideales) del equipo. Si este ya ha trabajado junto se puede utilizar la velocidad media en anteriores iteraciones, si no lo mejor es hacer una estimación con alguna de las técnicas existentes para ello y ir ajustándola a medida que avance el proyecto.
  • Priorizar las historias de usuario, de manera que siempre esté claro cual es la siguiente a desarrollar. Una buena manera de asegurar esto es utilizando el enfoque de Henrik Kniberg en su muy recomendable Scrum and XP For The Trenches, donde utiliza el atributo importancia, en lugar de prioridad. La priorización la decide el Product Owner, pero es aconsejable que escuche la opinión de los miembros del equipo, sobre todo en cuestiones relativa a la secuenciación de las historias de usuario.
  • Seleccionar las historias y la fecha de entrega, en base a la velocidad y a la estimación realizada. Si el factor mas importante en el proyecto es la fecha de entrega, lo que se hará será calcular el conjunto de funcionalidades que se preveen tener listas para esa fecha, si por el contrario, el factor importante son las funcionalidades mínimas, se deduce la fecha estimada de finalización.
  • Comprobar que se cumplen los criterios de éxito del proyecto. Si no es así se debe volver a los procesos de estimación y priorización, haciendo los cambios necesarios (subdividir historias en otras mas pequeñas, cambiando la priorización...) hasta conseguir dar con un grupo de funcionalidades para la entrega que cumpla con los criterios de éxito.

Otras cosa a tener en cuenta durante el proceso es la decisión de determinar por adelantado que funcionalidades se desarrollarán en cada iteración, o por el contrario sólo se deciden las funcionalidades a entregar en la iteración más próxima (de entre todas las planeadas para la entrega)

Cómo punto final, remarcar que es importantísimo que la planificación no sea un "papel colgado en la pared", sinó que hay que ir actualizándola y revisándola con cierta frecuencia, de manera que se pueda conocer rápidamente las desviaciones o los cambios que ocurren. Una buena opción es replanificar al inicio de cada iteración , justo antes de la planificación de la misma.

Aprovecho para hacer referencia al post anterior Calendario de Sprint
relacionando el concepto aquí explicado, con una de las reuniones propuestas en el mismo. Concretamente, la planificación de entregas seria lo que en el calendario llamabamos Project Planning (aunque en algunas organizaciones podría ser una fusión de las dos primeras reuniones explicadas, Project Review y Project Planning)

En el siguiente Post, mostraré un ejemplo práctico de cómo realizar la Planificación de Entregas de un proyecto sencillo.