Enumérer de façon asynchrone (C# 8.0)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.

C# 8.0 apporte un cas d’utilisation supplémentaire au pattern async/await en permettant d’énumérer de façon asynchrone (i.e. asynchronous streams).

Rappels concernant yield

Pour rappel, le mot-clé yield a été introduit en C# 2.0 de façon à créer un énumérateur à la volée et de contrôler l’énumération avec:

  • yield return pour renvoyer un objet à la volée lors de l’énumération.
  • yield break pour arrêter l’énumération.

Par exemple, si on considère la fonction suivante:

public int[] GetRandomNumbers(int numberCount)  
{  
  Random random = new Random();  
  var numbers = new int[numberCount];  
  for (int i = 0; i < numberCount; i++)  
  {  
    int number = random.Next(0, 100);  
    Console.WriteLine($"Generating {number}");  

    numbers[i] = number;  
  }  

  return numbers;  
}   

A l’exécution, tout le tableau de nombre devra être complêté pour sortir de la fonction et pour commencer l’énumération dans la boucle foreach:

foreach (var randomNumber in GetRandomNumbers(10000))  
{  
  Console.WriteLine(randomNumber);  
  if (randomNumber == 3)  
  {  
    Console.WriteLine("3 found");  
    break;  
  }  
}  

Tant que la fonction GetRandomNumbers() n’a pas terminé son exécution, l’énumération ne peut pas commencer. Ainsi si randomNumber est égal à 3 à la 1ère itération, on aura généré 9999 nombres inutilement puisqu’ils ne serviront pas pour le reste de l’exécution:

Generating 3
Generating 32   
Generating 78  
...  
Generating 53  
3 found  

Si on utilise yield, l’implémentation devient:

public IEnumerable<int> GetRandomNumbers(int numberCount)  
{  
  Random random = new Random();  
  for (int i = 0; i < numberCount; i++)  
  {  
    int number = random.Next(0, 100);  
    Console.WriteLine($"Generating {number}");  
    yield return number;  
  }  
}  

A l’exécution de la boucle foreach, l’énumération commence sans exécuter GetRandomNumbers() complêtement. Chaque itération va exécuter le contenu de GetRandomNumbers(), si randomNumber est égal à 3 à la 1ère itération, le contenu de GetRandomNumbers() ne sera exécuté qu’une seule fois:

Generating 3
3 found  

Enumérer de façon asynchrone

C# 8.0 permet d’effectuer une énumération avec yield de façon asynchrone. Dans la fonction dans laquelle se trouve yield, la syntaxe doit être du type:

async IAsyncEnumerable EnumerationFunction(...)  
{  
  // ...  

  await ... // Code exécuté de façon asynchrone  
  yield return ... // Valeur renvoyée lors de l'énumération
}  

Syntaxe de l’énumération

Avec await foreach

Pour effectuer l’énumération asynchrone, on peut utiliser une boucle await foreach:

await foreach (var item in EnumerationFunction())  
{  
  // ...  
}  

Par exemple, si on reprend l’exemple précédent:

public async IAsyncEnumerable<int> GetRandomNumbers(int numberCount)  
{  
  Random random = new Random();  
  for (int i = 0; i < numberCount; i++)  
  {  
    int number = await Task.Run(() => random.Next(0, 100)); // Code exécuté de façon asynchrone
    Console.WriteLine($"Generating {number}");  
    yield return number; // Valeur renvoyée lors de l'énumération
  }  
}  

L’énumeration avec await foreach peut être effectuée de cette façon:

await foreach (var randomNumber in GetRandomNumbers(10000))  
{  
  Console.WriteLine(randomNumber);  
  if (randomNumber == 3)  
  {  
    Console.WriteLine("3 found");  
    break;  
  }  
}  

Avec une itération manuelle

Dans le cas où on itère de façon manuelle c’est-à-dire sans utiliser foreach, on peut utiliser:

Dans le cas de l’exemple précédent, le code deviendrait:

await using (IAsyncEnumerator<int> enumerator = GetRandomNumbers(10000).GetAsyncEnumerator())  
{  
  while (await enumerator.MoveNextAsync())  
  {  
    if (enumerator.Current == 3)  
    {  
      Console.WriteLine("3 found");  
      break;  
    }  
  }  
}  

Cette syntaxe dispose l’objet IAsyncEnumerator<T> de façon asynchrone avec await using (voir “Disposer des objets de façon asynchrone” pour plus de détails).

Implémentation avec ConfigureAwait(false)

Il est possible de rajouter ConfigureAwait(false) pour optimiser le code en évitant d’utiliser le contexte d’exécution d’origine lors de l’exécution de la continuation avec async/await.

Par exemple:

  • Dans une boucle await foreach:
    await foreach (var randomNumber in GetRandomNumbers(10000).ConfigureAwait(false))  
    {  
      // ...  
    } 
    
  • Dans le cas d’une itération manuelle:
    IAsyncEnumerator<int> enumerator = GetRandomNumbers(10000).GetAsyncEnumerator();  
    await using (var _ = enumerator.ConfigureAwait(false))  
    {  
      // ...  
    }  
    

    Ou

    IAsyncEnumerator<int> enumerator = GetRandomNumbers(10000).GetAsyncEnumerator();  
    await using var _ = enumerator.ConfigureAwait(false);  
    
    // ...  
    

Utiliser un CancellationToken

On peut introduire un CancellationToken pour interrompre l’exécution du code asynchrone.

Par exemple:

  • Dans une boucle await foreach:
    var tokenSource = new CancellationTokenSource();  
    await foreach (var randomNumber in GetRandomNumbers(10000).WithCancellation(tokenSource.Token))  
    {  
      // ...  
    }  
    
  • Dans le cas d’une itération manuelle:
    var tokenSource = new CancellationTokenSource();  
    var token = tokenSource.Token; 
    await using (var enumerator = GetRandomNumbers(10000).WithCancellation(token).GetAsyncEnumerator())  
    {  
      // ...  
    }  
    

    Ou

    await using var enumerator = GetRandomNumbers(10000).WithCancellation(token).GetAsyncEnumerator();   
    // ...  
    

Leave a Reply