Async & await: comment éviter de manipuler des tâches
async & await
async et await sont deux mot-clés qui, utilisés conjointement, permettent la délégation de la synchronisation des tâches asynchrones au CLR (Common Language Runtime).
Un thread
Les systèmes d'exploitation modernes - grâce aussi à l'apparition des processeurs multi-coeurs - peuvent exécuter plusieurs tâches simultanément à d'autres dans un même programme.
Ainsi, un process (programme disposant de son propre espace mémoire) peut exécuter des sous-programmes lui appartenant et s'exécutant de manière asynchrone (non séquentielle).
Ces sous-programmes asynchrones se nomment des threads.
Unité d'exécution indépendante
Un thread est une unité d'exécution indépendante. C'est-à-dire qu'un thread s'exécute parallèlement à d'autres.
La particularité d'un thread est qu'il appartient au process qui l'a exécuté. Ainsi, un thread n'a accès qu'à l'espace mémoire de son process parent !
Définition d'un thread (français)
![]()
Différence entre un process et un thread (anglais)
Voici un exemple de code qui utilise les threads
using System;
using System.Threading;
namespace SimpleThread
{
class Program
{
static void Main(string[] args)
{
// Create a thread
Thread thread = new Thread(new ThreadStart(() =>
{
// Here is the code executed by the thread
Random rng = new Random();
Int32 waitTime = rng.Next(1000, 5000);
// Wait between 1000 ms (1 s) and 5000 ms (5 s)
System.Threading.Thread.Sleep(waitTime); // Simulates a long computation
Console.WriteLine("I waited " + waitTime / 1000.0d + " seconds");
}));
// Launch the thread asynchronously
thread.Start();
// Do some stuff while the thread is running
// [...]
Console.WriteLine("Waiting for the thread to finish ...");
// Wait for the thread to finish before ending the program
thread.Join();
}
}
}
Incertitudes
Les threads c'est magique ! On devrait en utiliser partout !
Stooop ... C'est là où je t'arrête.
Utiliser le multi-threading pour optimiser les temps de traitement semble tentant mais attention: c'est extrêmement technique et on peut passer beauuucouup de temps à comprendre pourquoi les threads ne se synchronisent pas.
Le multi-threading soulève aussi des problèmes de sécurité et de stabilité lorsque la synchronisation entre eux n'est pas correcte.
Et devinez quoi: synchroniser des threads entre eux est une galère lorsqu'on peut faire plus simple.
Quand devrais-je alors utiliser le multi-threading ?
Quand le besoin s'en fait ressentir. C'est-à-dire lorsque les performances d'un programme séquentiel ne conviennent plus à son utilisation (exemple: une page qui met 3 secondes à se charger, le cas de multiples connexions simultanées ...).
Voici un exemple de plusieurs threads se synchronisant grâce à l'utilisation d'un mutex:*
using System;
using System.Collections.Generic;
using System.Threading;
namespace SimpleThread
{
class Program
{
static void Main(string[] args)
{
IList<Int32> podium = new List<Int32>();
Mutex mutex = new Mutex();
ThreadStart threadDelegate = new ThreadStart(() =>
{
Random rng = new Random();
Int32 waitTime = rng.Next(1000, 5000);
System.Threading.Thread.Sleep(waitTime); // Simulates a long computation
mutex.WaitOne(); // Here the mutex get locked
Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId + " has locked the mutex");
podium.Add(System.Threading.Thread.CurrentThread.ManagedThreadId); // Here we modify the podium
mutex.ReleaseMutex(); // Then releasing the mutex for other threads to write in it
});
// Creating two threads
IEnumerable<Thread> manyThreads = new List<Thread> { new Thread(threadDelegate), new Thread(threadDelegate) };
foreach (Thread thread in manyThreads)
{
// Lauch the two threads asynchronously
thread.Start();
}
// Do some stuff while the threads are running
// [...]
Console.WriteLine("Waiting for the threads to finish ...");
// Waiting for the two threads to finish
foreach (Thread thread in manyThreads)
{
thread.Join();
}
Console.WriteLine("The winner is: " + podium[0]);
}
}
}
Deadlocks
Le principal risque que l'on encourt à utiliser des threads se nomme le deadlock. Il s'agit d'un cas où deux ou plusieurs threads interdépendants sont tous deux verouillés, l'un attendant les informations d'un des autres pour continuer, l'autre attendant l'information des uns pour continuer. On est alors dans une situation où aucun des deux threads ne peuvent répondre, et ça arrive bien plus souvent qu'on ne l'imagine !

Article wikipédia sur l'interblocage - lire l'introduction est suffisant
Une tâche
Puisque les threads sont difficiles à manipuler du fait de leur caractère indépendant du processus qui les lance, Microsoft a décidé d'en simplifier l'écriture et la gestion en créant les tâches.
Qu'est-ce qu'une tâche apporte de plus ?
Un thread en mieux
Une tâche est un thread, mais en mieux.
Pourquoi ? Car un des principal défaut d'un thread est qu'il est très difficile d'obtenir des données sur son état actuel (lancé, stoppé, planté, en cours ...). En effet, pour qu'un thread signale sa terminaison ou son avancement au process, il faut utiliser des mutex ou la programmation événementielle. Donc, on doit gérer la synchronisation à la main avec des bricolages.
Une tâche est un thread qui dispose d'information sur son état d'avancée (pas encore exécuté, en cours d'exécution, terminé). Disposer de ces informations simplifie énormément la gestion de la synchronisation des threads car nous pouvons connaître dans le process parent l'état d'exécution de ses threads.

Tutoriel sur les objets de class Task
Voici un exemple très proche des exemples sur les threads ci-dessus (mais réecrit en utilisant les tâches):
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SimpleThread
{
class Program
{
static void Main(string[] args)
{
IList<Task<Int32>> manyTasks = new List<Task<Int32>>();
// Creating three tasks
for (Int32 tasksCount=0; tasksCount < 3; tasksCount++)
{
Task<Int32> newTask = new Task<Int32>(() =>
{
Random rng = new Random();
Int32 waitTime = rng.Next(1000, 5000);
System.Threading.Thread.Sleep(waitTime);
return waitTime;
});
manyTasks.Add(newTask);
}
// Launch each task
foreach (Task<Int32> task in manyTasks)
{
task.Start();
}
// Aggregate all tasks in one
Task<Int32[]> allTasks = Task.WhenAll(manyTasks);
// Do some stuff while tasks are running
// [...]
Console.WriteLine("Waiting for tasks to finish ...");
// Waiting for the three tasks to
allTasks.Wait();
Console.WriteLine("Here are the times waited ...");
Console.WriteLine(String.Join(", ", allTasks.Result)); // Display milliseconds waited for all tasks
}
}
}
Manipuler une tâche n'est cependant pas aussi simple qu'il n'y paraît et c'est pour cela que Microsoft nous a (encore une fois) simplifié la vie en implémentant les mot-clés await et async.
Mot-clé await
await est un mot-clé puissant qui simplifie encore une fois la mise en oeuvre du parallélisme et de la concurrence.
Ainsi, lorsqu'on utilise await, la gestion de la synchronisation des tâches est faite automatiquement par le CLR.
Du coup, on ne sait pas quand le thread s'exécute ?!
Oui, exactement. Nous ne connaissons pas la façon avec laquelle le process va synchroniser ces threads, mais ce qui est quasiment certain c'est qu'il le fera mieux que beaucoup de développeurs.
Ainsi, lorsque l'on précède une tâche du mot-clé await, on va directement récupérer le résultat sans se soucier de la tâche en elle-même: la gestion de la synchronisation est laissée à C# !
Pourquoi async ?
async est un mot-clé - qui peut être ajouté à la signature d'une méthode - indiquant au CLR (Common Language Runtime) que le code défini dans son corps devra être traité de manière asynchrone.
Ainsi, dès lors que l'on précède la méthode du mot-clé async, le CLR saura que cette méthode en particulier doit être traitée de façon parallèle et concurrentielle par le CLR et non par le développeur.
Comment synchroniser les tâches en utilisant le mot-clé await ?
Voici le dernier exemple, réécrit en utilisant await:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SimpleThread
{
class Program
{
static async Task<Int32> Main(string[] args)
{
// Creating three task
Random rng = new Random();
Int32 waitTime = rng.Next(1000, 5000);
// Creating a task that will wait
await Task.Delay(waitTime);
// The program will end after the preceding task terminates
Console.WriteLine("The task has waited " + waitTime);
return 0;
}
}
}
Challenge
Tu vas créer deux tâches dont chacune attend une durée aléatoire. Il va falloit exécuter tes tâches en utilisant await et en définissant la méthode englobante async pour informer le CLR que la méthode utilise await.
Critères de validation
- Deux tâches - chacune attendant une durée aléatoire de temps avant d'afficher le délai attendu
- Les des deux tâches sont exécutées en utilisant le mot-clé await