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

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *