AsyncWorkManagerHelper para WP7

Hace mucho que tenía este artículo y este código en el tintero por repasar y publicar. Se trata de un Helper para ejecutar tareas en un agente de WP7 de manera asíncrona.

Hace 1 año aproximadamente, Peter Torr publicaba un artículo en tres partes explicando el trabajo con los agentes en background para WP7. Los enlaces a las tres partes son:

1- Background Agents – Part 1 of 3
2- Background Agents – Part 2 of 3
3- Background Agents – Part 3 of 3

En la segunda parte de la seria se discute un Helper para realizar tareas en segundo plano y esperar que estas terminen para poder notificar al agente el resultado de la ejecución.

El helper da una limpieza a nuestro código muy buena y nos permite olvidarnos de todo el proceso asíncrono. Sin embargo, si queremos extender el uso del helper más allá de un agente y utilizarlo en nuestra aplicación, encontramos que carece de alguna funcionalidad.

1- No se tiene control de lo que va sucediendo con las tareas que se van ejecutando hasta que no termina todo el proceso.
2- Si yo necesito que las tareas se ejecuten de 1 en 1 o de 2 en 2, o de N en N, tampoco tendría el control sobre ello.

Entiéndase que el helper fue escrito para ayudar con las tareas en background de un agente y en estos casos pocas veces importa lo que está sucediendo hasta que no termina todo el proceso.

Dispuesto a poder utilizar el código del Helper en cualquier tarea asíncrona que necesite ejecutar mi aplicación de WP7, le hice algunas modificaciones al código.

FIFO para las tareas.

First In, First Out. El código de Peter ejecuta directamente cada tarea cuando se adiciona la misma al Helper. En realidad estas ejecuciones son bloqueadas hasta que un ManualResetEvent es seteado cuando se ejecuta el Start o el WaitAll

/// <summary>
/// Schedules a work item to begin
/// </summary>
/// <param name="work">The work to perform</param>
/// <remarks>The worker thread is blocked on the startEvent so that the work
/// doesn't complete before the client is ready to handle the completion event
/// </remarks>
void BeginWorkItem(WorkloadInfo<TParam, TResult> work)
{
  lock (this)
  {
    outstandingItems++;
  }

  ThreadPool.QueueUserWorkItem(delegate
  {
    // Wait until it's OK to start
    startEvent.WaitOne();
    try
    {
      // Method is responsible for completing itself if it didn't fail
      work.Method(work.Parameter, work);
    }
    catch (Exception ex)
    {
      // Complete with a failure case
      work.NotifyFailure(ex);
    }
  });
}

Si queremos controlar la cantidad de tareas ejecutándose a la vez, este mecanismo no nos vale, por lo que he creado un Queue (FIFO en NET) para almacenar las tareas y poder controlar su ejecución.

/// <summary>
/// FIFO to Tasks
/// </summary>
private readonly Queue<WorkloadInfo<TParam, TResult>> _queue;

El la clase AsyncWorkManager ahora tiene 2 constructores, en uno de ellos podemos indicar el límite de tareas que se ejecutan simultáneamente.

Para poder conocer el estado de cada tarea en el momento en que se ejecutan, se ha adicionado un evento (OnCompleteWorkItem) a la clase WorkLoadInfo que es disparado justo al concluir la ejecución de la misma.

/// <summary>
/// Completes the work item with a successful result
/// </summary>
/// <param name="result">The result of the operation</param>
/// <remarks>This method is called by the worker Method once it has completed its task</remarks>
public void NotifySuccess(TResult result)
{
    MarkAsComplete();
    Result = result;

    if (OnCompleteWorkItem != null) OnCompleteWorkItem(this, EventArgs.Empty);
    _parent.CompleteWorkItem();
}

/// <summary>
/// Completes the work item with an error
/// </summary>
/// <param name="error">The error to report</param>
/// <remarks>This method is called by the worker Method if it fails its task</remarks>
public void NotifyFailure(Exception error)
{
    MarkAsComplete();
    Error = error;

    if (OnCompleteWorkItem != null) OnCompleteWorkItem(this, EventArgs.Empty);
    _parent.CompleteWorkItem();
}

La clase AsyncWorkManager tiene dos métodos por los cuales se puede iniciar el proceso. Start y WaitAll.

Start

El método inicia la ejecución de todas las tareas y no espera por su finalización. En la versión original del Helper el Start se ejecuta en el mismo hilo desde el cual es llamado y, si en nuestro caso queremos controlar la cantidad de tareas que se van ejecutando esto tampoco nos vale, así que también se ha modificado para que se ejecute en un ThreadPool diferente.

/// <summary>
/// Starts performing work, if not already happening.
/// </summary>
/// <remarks>This method is implicitly called by the WaitAll methods</remarks>
public void Start()
{
    _startNewEvent.Set();

    lock (this)
    {
        if (_queue.Count == 0)
        {
            CompleteWorkload();
            return;
        }

        // Work has already been completed
        if (_completionEvent.WaitOne(0)) return;

        // Reset the completion event to be waited on
        _completionEvent.Reset();

        _outstandingItems = _queue.Count;
    }

    ThreadPool.QueueUserWorkItem(delegate
    {
        // Release the threads waiting to do work
        while (true)
        {
            if (_executingOperations == _concurrentOperations && _concurrentOperations != 0) _startNewEvent.Reset();

            _startNewEvent.WaitOne();

            var work = _queue.Dequeue();
            BeginWorkItem(work);

            if (_queue.Count == 0) break;
        }
    });
}

Una particularidad del cambio realizado es que si usamos el constructor sin parámetros de la clase AsyncWorkManager, no se controla la concurrencia y todas las tareas son ejecutadas a la vez.

WaitAll

Este método tiene dos sobrecargas, una sin parámetros que espera infinitamente a que todas las tareas se hayan ejecutado (ojo que esto detiene el hilo en el que se haga la llamada al método) y otro que espera un tiempo (timeout) y aborta en caso de que todas las tareas no se hayan finalizado.

Después de los cambios, un ejemplo de cómo se podría usar el Helper sería este:

var calendarsWorkManager = new AsyncWorkManager<Account, IEnumerable<Calendar>>(concurrentsTask:1); 

GetAccounts(delegate(object o, AccountsEventArgs args)
{
    foreach (var account in args.EntityList)
    {
        switch (account.Source)
        {
            case Enums.SourceProvider.Google:

                var rGoogle = calendarsWorkManager.AddWorkItem(SyncGoogleCalendars, account);
                rGoogle.OnCompleteWorkItem += delegate
                {
                    if (rGoogle.Error == null) calendarList.AddRange(rGoogle.Result);
                };

                break;
            case Enums.SourceProvider.Live:

                var rLive = calendarsWorkManager.AddWorkItem(SyncLiveCalendars, account);
                rLive.OnCompleteWorkItem += delegate
                {
                    if (rLive.Error == null) calendarList.AddRange(rLive.Result);
                };

                break;
            case Enums.SourceProvider.Exchange:

                calendarList.Add(ExchangeCalendar(account));

                break;
        }
    }

    calendarsWorkManager.WorkComplete += (sender, a) => onCalendarList(this, new CalendarsEventArgs(calendarList));
    calendarsWorkManager.Start();
});

En principio la funcionalidad es la misma, excepto que podemos conocer y hacer algo cuando cada tarea se termina de ejecutar (onCompleteWorkItem), podemos indicar la cantidad de tareas ejecutándose a la vez (concurrentsTask:1) y ejecutar el inicio del proceso mediante Start sin que el hilo quede bloqueado.

Adjunto el código del Helper modificado por si a alguien le vale.

Un salu2

#region en C#

Acabo de leer en twitter un comentario que dice:

#region in C# only has 2 purposes: to add unnecessary noise, or when you think it helps, it’s actually telling you how much your class sucks because if you feel the need to ‘group’ things into regions, you generally need to separate the code into multiple classes”

El comentario se refiere a dos formas diferentes de usar #region, uno sería quien usa las regiones para identificar zonas privadas o públicas, o para identificar constructores del resto de la lógica en una clase y, el otro grupo se refiere a quienes lo usan para agrupar funcionalidad o lo que es lo mismo, lógicas diferentes dentro de una misma clase.

Al primer grupo, le define el uso de regiones como “ruido”, un calificativo desde mi punto de vista que pudiera ser discutible Winking smile 

Sobre el comentario hacia el segundo grupo no se le puede quitar una pizca de razón. Si una clase es lo suficientemente grande como para que te sea “molesta a la vista”, puedes estar seguro que necesitas un refactoring antes que varios #region

… aquí tienes un claro ejemplo de lo que nunca se debería hacer:

Untitled

cu…

[OT] Return to live…

Hace ya bastante tiempo que un día sí y otro también, solo sabemos escuchar hablar sobre las nuevas tecnologías que nos vienen desde Microsoft: Metro, W8, WP8, nueva versión de NET, de MVC, de EF… 

Si a eso le sumamos andadoras por WebGL con three.js, Kinect con javascript y HTML 5 que últimamente me tocan porque sí… poco tiempo queda para escribir algo.

Aprovechando que agosto parece que viene más “relajado” y que finalmente, después de cambiar a WP, pienso empezar a publicar cosas en mi blog (www.odelvalle.com) sería buen momento para retornar a la vida… Smile

Para no dejar abandonado (no podría) a geeks.ms, este primer post viene siendo algo así como un [TestMethod] desde Live Writer hacia el resto del mundo

 

Pronto más…
salu2