Escribiendo código concurrente de alto rendimiento con monitores

Las aplicaciones multi hilo o concurrentes van a ser el siguiente gran problema para los desarrolladores, y tenemos que estar listos para este cambio tan grade. En el .NET Framework 4, Microsoft incluye una serie de nuevas APIs que ayudan al desarrollador en la creación de aplicaciones concurrentes. Eso no significa que tengamos que despreocuparnos del código concurrente sino que a partir de ahora va a ser más fácil hacerlo.

Actualmente estoy trabajando en una aplicación que hace un uso intensivo de la API de TPL. Básicamente la aplicación genera una serie de Task que administran la ejecución de una serie de reglas aplicadas a una Uri. Además de eso mantengo una lista con la colección de Task creadas por mi aplicación, para contabilizar cuantas están ejecutándose y una serie de estadísticas de velocidad de la aplicación.

Esto significa que tenemos una UI donde vamos mostrando los datos, creada en WPF 4 que necesita comunicarse con este código para obtener todas las tareas que se están ejecutando en ese momento. Teniendo en cuenta que estoy usando una simple List<Tast> para guardar la referencia de todas las tareas, necesito una manera de bloquear la lsita para añadir elementos a la lista, enumerar la lista y eliminar elementos. Pero queiro que este bloqueo dure lo menos posible para hacer que mi aplicación se ejecute lo más rápido posible y además quiero solo bloquear para las operaciones importantes añadir y eliminar tareas

Así que tengo dos operaciones importantes, crear una nueva tarea y añadirla a la lista, y cuando la tarea termina eliminar esa tarea del servidor, pero tengo que tener en cuenta que múltiples tareas pueden estar ejecutándose a la misma vez.

La creación de tareas es responsabilidad de un temporizador que está configurado para ejecutarse para Segundo y si el número de tareas es menor que el número de tareas mínimas genera más tareas y cada tarea creada la añade al sistema. Teniendo en cuenta que este temporizador es ejecutado cada segundo, necesito bloquear mi lista, hacer mi trabajo y después desbloquear. Teniendo en cuenta que el bloqueo para acceder a la lista puede ser mayor que el tiempo del temporizador porque otros threads pueden estar usando la lista necesito establecer un tiempo máximo de bloqueo porque si no lo que conseguiré será una clásico convoy de bloqueos en mi aplicación.

Teniendo en cuenta esto dentro del método de mi temporizador utilizo la nueva API de Monitor.TryEnter de .NET 4 que intenta, para un número de milisegundos, adquirir el bloqueo exclusivo y atómicamente establecer un valor que indica si el bloqueo se ha obtenido o no. En mi caso he establecido el temporizador en 400ms, porque en mi método hago más cosas que trabajar con esa lista.

bool taken = false;
try
{
    Monitor.TryEnter(task, 400, ref taken);
    if (taken)
    {
        if (!cancelationTokenSource.IsCancellationRequested)
        {
            var count = (from p in task
                         where p.Status == TaskStatus.Running || p.Status == TaskStatus.WaitingToRun
                         select p).Count();

        }
    }
}
finally
{
    if (taken)
    {
        Monitor.Exit(task);
    }
}

Como se puede observar en el código estoy llamando a la función de Monitor.TryEnter dentro de un bloque de try/catch, y en el bloque de finllay si el bloqueo se ha adquirido lo libero llamando a la función Monitor.Exit. Lo hago de esta manera para asegurarme que si durante la ejecución de mi código se lanza una excepción el código de finally siempre será ejecutado y el bloqueo liberado. Si no lo hiciese de esta manera podría causar que se nunca se liberase el bloqueo y tener un deadlock.

El otro escenario donde trabajo con la lista es para obtener el número de tareas ejecutándose, y hago lo mismo dentro de accesor get de una propiedad, pero esta vez lo tengo configurado a 250ms. Si el bloqueo se adquiere satisfactoriamente, lo que hago es copiar la lista en una lista nueva par después de manera segura trabajar sobre esa nueva lista.

public List<Task> Tasks
{
    get
    {
        bool taken = false;
        List<Task> list = null;
        try
        {
            Monitor.TryEnter(task, 250, ref taken);
            if (taken)
            {
                list = new List<Task>(task.ToArray());
            }
        }
        finally
        {
            if (taken)
            {
                Monitor.Exit(task);                         
            }
        }
        return list;
    }
}

Esta solución es simple y mantiene tu código con la tasa de contención más baja posible, pues no esperas infinitamente en todos los casos, solo en los más importantes.

A parte de esto podría haber hecho un wrapper de List<T> con ReaderWriterLockSlim para controlar las operaciones de lectura y escritura durante el bloqueo, pero ReaderWriterLockSlim está pensado para trabajar con varios lectores y solo un escritor y en mi aplicación tengo múltiples escritores y solo un lector.

Saludos.

Luis.

3 comentarios en “Escribiendo código concurrente de alto rendimiento con monitores”

  1. Hola Paco!

    Gracias por tu comentario, lamentablemente no puedo compartir ese código porque es parte de una aplicación que estamos desarrollando para Microsoft así que no es posible.

  2. Hola Luis, te estuve escuchando en la charla que distes en deusto de TPL y Mef y la cuestión es que cacharreando con for.paralell llego a un outofmemoryexception, lo que quería saber es como asignar mas memoria al framework, y cual es el nombre de la herramienta que utilizabas para analizar el performance (nº de hilos, memoria usada….) en la presentación, gracias!.

Deja un comentario

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