Twitter Facebook RSS Feed

jueves, 15 de octubre de 2009 a las 15:36hs por Gustavo Cantero (The Wolf)

Future o Task<T>

En la versión CTP existe una clase llamada Future<T>, la cual hereda de Task, e implementa un viejo concepto existente en multi-lisp: nos permite crear una tarea para obtener o calcular un valor, que ésta se ejecute en paralelo y, al momento de necesitarlo, obtener el resultado de la rutina llamada desde la tarea o, en caso de que aún no haya sido ejecutada, correrla en el thread actual y luego obtener su valor utilizando la propiedad Value. Un ejemplo sería algo como lo siguiente:

Future objValor = Future.Create(CalcularValor);
//...
//Hago otros cálculos
//...
int Resultado = objValor.Value;

Para lo cual el método CalcularValor debería ser parecido al siguiente:

private int CalcularValor()
{
    //Calculo el valor a devolver
    return resultado;
}

En .NET 4.0 la clase Future ya no existe, pero en cambio existe una sobrecarga de la clase Task: Task<T>, la cual nos permite realizar lo mismo que con Future pero solamente usando una sintaxis diferente:

Task objValor = new Task(CalcularValor);
//...
//Hago otros cálculos
//...
int Resultado = objValor.Result;

Nótese que aquí en lugar de usar la propiedad Value, al usar la versión con generic de la clase Task, disponemos de la propiedad Result.

Concurrencia y bloqueos

En el primer ejemplo que hicimos con paralelismo comenté que podría haber problemas en el uso de la variable cant2 porque era usada desde las distintos threads. Esta situación podría generar que el valor de la variable no fuera el correcto. Imaginemos que dos threads quieren incrementar el valor de esta variable al mismo tiempo, podría darse la siguiente situación:

Thread 1 Thread 2 Valor de cant2
leo el valor de cant2 (que es 0) 0
leo el valor de cant2 (que es 0) 0
cant2 = valor anterior (0) + 1 1
cant2 = valor anterior (0) + 1 1

Entonces, luego de intentar incrementar el valor de cant2 (que inicialmente era cero) desde ambos threads, el resultado es que la variable vale 1, cuando debería valer 2. Esta situación es conocida como “race condition”.
Para corregir esto podemos bloquear el uso de las variables con la sentencia “lock” de C# o “SyncLock” de Visual Basic. La utilización de esta sentencia previene el uso del objeto a bloquear por parte de otros threads, lo que en nuestro ejemplo generaría que el thread 2 esperaría hasta que el primero termine de leer y modificar la variable compartida antes de poder utilizarla. Esta sentencia toma como parámetro el objeto a bloquear, el cual libera al finalizar el bloque de código. En nuestro caso necesitamos otro objeto para utilizarlo como “marca” de bloqueo, ya que la variable es un entero y no puede utilizarse con el lock. El código de ejemplo de esto sería el siguiente:

int cant2 = 0;
object bloqueo = new object();
Parallel.For(0, 100000, (valor) =>
{
    if (valor < 2)
        lock (bloqueo)
        {
            cant2++;
        }
    else
    {
        bool divisible = false;
        for (int temp = 2; !divisible && temp < valor; temp++)
        {
            if (valor % temp == 0)
                divisible = true;
        }
        if (!divisible)
            lock (bloqueo)
            {
                cant2++;
            }
    }
});

Al ejecutar este código con bloqueo en mi PC y compararlo con el mismo código con paralelismo utilizado en el primer ejemplo, sucede algo curioso: sin bloqueo encontró 9591 números primos, mientras que con bloqueo encontró 9594. Eso quiere decir que se está dando la condición “race condition” en nuestro ejemplo.
Cabe mencionar que al utilizar la sentencia lock o SyncLock, cualquier thread que quiera utilizar o bloquear el objeto bloqueado quedará en espera hasta que se haya liberado.
Ahora bien, hay veces que necesitamos manejar distintas variables entre las distintas tareas, y bloquear todo al principio y liberarlo al final generaría que las tareas estén esperando la mayor parte de. Para reducir los tiempos de bloqueos y generar un código más legible (o sea, no tener locks por todas partes) el Framework de .NET dispone de la clase Interlocked, la cual posee varios métodos interesantes para la utilización de variables compartidas sin el problema de los bloqueos o la concurrencia de los threads, de los cuales mostraremos los más relevantes en la siguiente lista:

  • Add: permite sumar un valor a una variable
  • Decrement: permite restar un valor a una variable
  • Increment: incrementa el valor de una variable en uno
  • Exchange: devuelve el valor actual de una variable y le establece uno nuevo

Para nuestro ejemplo nos conviene utilizar el método Increment, con el cual nos quedaría lo siguiente:

int cant2 = 0;
Parallel.For(0, 100000, (valor) =>
{
    if (valor < 2)
        Interlocked.Increment(ref cant2);
    else
    {
        bool divisible = false;
        for (int temp = 2; !divisible && temp < valor; temp++)
        {
            if (valor % temp == 0)
                divisible = true;
        }
        if (!divisible)
            Interlocked.Increment(ref cant2);
    }
});

0 comentarios »

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.