¿Cómo podemos usar ¨semáforos¨en nuestros proyectos?

En muchas ocasiones, tenemos que ejecutar varios bloques de código al mismo tiempo.

En general, en estos casos, seguiríamos el patrón TAP (Task-based Asynchronus Pattern) y utilizaríamos Tasks.WhenAll con la colección de tareas a ejecutar. De esta forma ejecutaríamos simultáneamente todas las tareas:

var tasksList = new Task[] { task1, task2, task3, task4 };

await Task.WhenAll(tasksList);

El problema viene cuando esa colección es significativamente grande y ralentiza la UI de forma apreciable para el usuario. Esto se agrava si nuestro código se ejecuta en un dispositivo móvil.

Como solución, podríamos ejecutar esas tareas de una en una utilizando un simple away delante de cada una de ellas. Seguramente la UI quedaría a salvo, pero el tiempo que tardaría en ejecutarlas sería considerablemente superior (al fin y al cabo estamos ejecutándolas de una en una y en riguroso orden).

await task1(); await task2(); await task3(); await task4();

Otra solución más interesante sería implementar un bloque común que solo permita ejecutar un número de tareas simultáneas y el resto las encole para ir ejecutándolas conforme se terminen de procesar las anteriores. Esto lo implementaríamos con una sección crítica.

Para gestionar la sección crítica, utilizaremos un semáforo.

  • Este tipo de objeto tiene un constructor con un parámetro que le indica el número de tareas simultáneas que dejará pasar.
  • Tiene además un método wait que bloqueará la tarea hasta que las tareas en ejecución en la sección crítica sea inferior a la que se le ha indicado en el constructor.
  • Y finalmente tiene un método Release, que indica al semáforo que la tarea actual ya ha salido de la sección crítica y puede dejar pasar a otra.

Como veis, utilizar un semáforo es muy sencillo. Solo te obliga a pasar todas las tareas por un único punto que gestionará por nosotros si pausa o por el contrario ejecuta estas tareas.

private static SemaphoreSlim semaphore = new SemaphoreSlim(3);

private static int ProcessCount = 0;

 

protected async Task ExecuteWithMax(Func<Task> taskDelegate)

{

Debug.WriteLine($»Task ‘{taskName}’ waiting for the semaphore; Tasks waiting: {++ProcessCount}»);

 

await semaphore.WaitAsync();

 

Debug.WriteLine($»Task ‘{taskName}’ entering semaphore; Tasks waiting: {–ProcessCount}»);

await taskDelegate();

 

var count = semaphore.Release();

Debug.WriteLine($»Task ‘{taskName}’ release the semaphore; Semaphore count: {count}»);

}

La clase concreta que utilizaremos como semáforo es SemaphoreSlim (hasta los semáforos tienen que ser finos ;)), que además de ser ligero, nos va a permitir utilizar el patrón TAP.

Ejemplo:

Para probarlo, hemos creado ocho tareas que duran de uno a ocho segundos (desde luego no consumen nada de procesamiento, pero para ilustrar el ejemplo nos vale):

private async Task task1()

{

Console.WriteLine(«\ttask1 starting»);

await Task.Delay(1000);

Console.WriteLine(«\ttask1 ending»);

}

private async Task task2()

{

Console.WriteLine(«\ttask2 starting»);

await Task.Delay(2000);

Console.WriteLine(«\ttask2 ending»);

}

private async Task task8()

{

Console.WriteLine(«\ttask8 starting»);

await Task.Delay(8000);

Console.WriteLine(«\ttask8 ending»);

}

  • Primero las hemos ejecutado todas a la vez y, lógicamente tardan lo mismo que la tarea más lenta (8 segundos).
  • Luego las hemos ejecutado de una en una. Tarda en ejecutarse la suma de todas (36 segundos).
  • Finalmente las hemos vuelto a ejecutar todas a la vez, pero ahora pasan por nuestro semáforo (que está configurado para solo dejar pasar a tres). Ahora tardan 15 segundos, que es un tiempo bastante aceptable.

—————————————-

Tasks with semaphore

—————————————-

Task ‘task1’ waiting for the semaphore; Semaphore Waiters: 1

Task ‘task1’ entering semaphore; Semaphore Waiters: 0

‘task1’ starting

Task ‘task2’ waiting for the semaphore; Semaphore Waiters: 1

Task ‘task2’ entering semaphore; Semaphore Waiters: 0

‘task2’ starting

Task ‘task3’ waiting for the semaphore; Semaphore Waiters: 1

Task ‘task3’ entering semaphore; Semaphore Waiters: 0

‘task3’ starting

Task ‘task4’ waiting for the semaphore; Semaphore Waiters: 1

Task ‘task5’ waiting for the semaphore; Semaphore Waiters: 2

Task ‘task6’ waiting for the semaphore; Semaphore Waiters: 3

Task ‘task7’ waiting for the semaphore; Semaphore Waiters: 4

Task ‘task8’ waiting for the semaphore; Semaphore Waiters: 5

‘task1’ ending

Task ‘task1’ releasing semaphore; Free slots: 1

Task ‘task4’ entering semaphore; Semaphore Waiters: 4

‘task4’ starting

‘task2’ ending

Task ‘task2’ releasing semaphore; Free slots: 1

Task ‘task5’ entering semaphore; Semaphore Waiters: 3

‘task5’ starting

‘task3’ ending

Task ‘task3’ releasing semaphore; Free slots: 1

Task ‘task6’ entering semaphore; Semaphore Waiters: 2

‘task6’ starting

‘task4’ ending

Task ‘task4’ releasing semaphore; Free slots: 1

Task ‘task7’ entering semaphore; Semaphore Waiters: 1

‘task7’ starting

‘task5’ ending

Task ‘task5’ releasing semaphore; Free slots: 1

Task ‘task8’ entering semaphore; Semaphore Waiters: 0

‘task8’ starting

‘task6’ ending

Task ‘task6’ releasing semaphore; Free slots: 1

‘task7’ ending

Task ‘task7’ releasing semaphore; Free slots: 2

‘task8’ ending

Task ‘task8’ releasing semaphore; Free slots: 3

—————————————-

Total time: 15s.

—————————————-

¡Y esto es todo!

¡Espero que os haya gustado y que podáis aplicarlo a vuestros proyectos!

¡Hasta otra!

Escrito por Alberto Fraj Souto, XAML Technical Leader en Bravent