Exécution asynchrone avec "await" et "async" en 5 min

"Await" et "async" ne sont pas des mot-clé qui permettent la création de thread mais ils permettent d’indiquer au compilateur:

  • les méthodes pour lesquelles l’exécution sera asynchrone en utilisant "async",
  • les endroits dans le code où on va attendre la fin de l’exécution d’une tâche en utilisant "await".

L’asynchronisme avec Async/Await permet, par exemple, d’éviter d’avoir un thread qui tourne en plus du thread principal pour attendre la réponse d’une tâche ou d’un traitement. L’asynchronisme va permettre d’exécuter du code quand la tâche ou le traitement ont fini de s’exécuter (équivalent d’une continuation). Il n’est donc pas nécessaire d’avoir un thread d’attente.

Plus de détails sur MSDN.

Async

Ce mot-clé va indiquer que la méthode contient du code qui peut être exécuté de façon asynchrone. Il s’applique sur des méthodes qui renvoient une tâche Task<TResult> ou Task.

Le type Task permet de récupérer des exceptions, d’avoir des fonctions du type Task.Wait(), Task.WaitAny() etc…

On a indiqué précedemment que pour utiliser ce mot-clé sur une méthode, le corps de cette méthode doit contenir du code à exécuter de façon asynchrone. Dans la majorité des cas, on aura à utiliser le mot-clé "await" pour attendre la fin de l’exécution.

Si le corps d’une méthode avec "async" ne contient pas le mot clé "await", il y aura un message d’avertissement du compilateur car cela signifie que le code sera exécuté de façon synchrone.

Par exemple, le code suivant est exécuté de façon synchrone i.e. le thread principal est bloqué jusqu’à la fin du "Sleep" même avec la présence du mot-clé "async":

public async Task WaitSynchronously()  
{  
  // Add a using directive for System.Threading.  
  Thread.Sleep(10000);  

  return "Finished";  
} 

En revanche le code suivant sera exécuté de façon asynchrone i.e. le thread principal ne sera pas bloqué:

public async Task WaitAsynchronouslyAsync()  
{  
  await Task.Delay(10000);  

  return "Finished";  
}

Plus de détails sur MSDN.

Tout ce qui se trouve après l’instruction "await" dans la fonction "async" sera considéré comme une continuation. Ainsi le code ne sera pas bloquant et ce qui se trouve après le "await" dans la fonction sera exécuté quand la tâche sera terminée.

De même si on utilise une boucle:

Task GetWebPageAsync(string uri) 
{ 
  ... 
}

async void Test() 
{ 
  for (int i = 0; i < 5; i++) 
  { 
    string html = await GetWebPageAsync("…"); 
    Console.WriteLine(html): 
  } 
}

Ainsi l’exécution des boucles se feront sans blocages. Ce qui se trouve après la ligne avec le "await" sera exécuté comme une continuation quand le GetWebPageAsync() aura terminé son exécution.

Pour arriver à faire ce traitement, .NET utilise des machines à états c’est-à-dire qu’il sauvegarde l’état à chaque fois qu’une instruction est lancée avec un "await". Ainsi les valeurs des variables à ce moment sont celles au moment du lancement de l’instruction "await".

Await

Ce mot-clé permet d’indiquer que l’exécution sera arrêtée jusqu’à ce que la méthode retourne son résultat. En fait on peut considérer que tout ce qui se trouve après le "await" dans le code pourrait être une "continuation" (voir plus haut).

Plus de détails sur MSDN.

Exemple simple

public static async Task DownloadContent() 
{ 
  using (HttpClient client - new HttpClient()) 
  { 
    string result = await client.GetStringAsync("http://www.microsoft.com"); 
    return result; 
  } 
} 

static void Main(string[] args) 
{ 
  string result = DownloadContent().Result; 
}

La méthode "GetStringAsync" renvoie une tâche et donc il est possible d’utiliser "await" avec cette méthode.

Utilisation de async et await

La plupart du temps ces mot-clé sont utilisés avec des méthodes asynchrones de l’API .NET:

  • leur nom se terminent par "Async": HttpClient.GetStringAsync(), StreamWriter.WriteAsync().
  • des fonctions d’un service WCF qui permettent les exécutions asynchrones (avec l’attribut "OperationContractAttribute(AsyncPattern=true)" (MSDN).

Plus de détails sur MSDN.

Remarques importantes

Défauts

L’utilisation de async/await sous-entend que du code est généré pour permettre l’asynchronisme. Ce code est lourd et est plus compliqué que pour du code synchrone. Donc si le temps de traitement de la tâche est inférieur à 500ms, il n’y a pas vraiment d’intérêt à utiliser async/Await.

SynchronizationContext

Le contexte de synchronisation est ce qui permet à des threads d’être synchronisé avec le thread principal (i.e. celui de l’interface graphique).

Certaines fonctions dans la classe Task permettent de ne pas utiliser du contexte de synchronisation:
Task.ConfigureAwait(bool continueOnCapturedContext):

  • true (par défaut): on poste une continuation vers le contexte actuel.
  • false: on ne poste pas de continuation, l’exécution de la tâche s’effectue dans le pool de thread.

On peut éviter d’effectuer une continuation pour des besoins de performances (puisqu’on ne fait pas une copie du contexte de synchronization).

Deadlock

Ne pas utiliser de continuation peut entraîner des deadlocks.
Par exemple:

async void button1_Click() 
{ 
  await DoWorkAsync().Wait(); 
} 

async Task DoWorkAsync() 
{ 
  await Task.Run(); 
  Console.WriteLine("Done Task"); 
}

Dans l’appel à DoWorkAsync(), on attends la fin de l’exécution de la tâche donc le thread bloqué jusqu’à la fin de la tâche. Dans la tâche avec le Console.WriteLine(), on veut faire appel au thread principal pour écrire une ligne or le thread principal est bloqué d’où le deadlock.

Recommandations dans l’utilisation d’async/await

Tout ce qui se trouve après le "await" est considéré comme une continuation donc toutes les variables locales sont converties en champs dans la machine à état. Donc plus il y a de variables et plus il y a de champs dans la machine à état.

Il faut éviter d’utiliser trop de variables locales après le "await" en particulier lorsque le "await" est dans une boucle.

Références

Programmation asynchrone avec Async et Await: https://msdn.microsoft.com/fr-fr/library/hh191443.aspx

Leave a Reply