Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.
Enumérer de façon asynchrone
Syntaxe de l’énumération
Avec await foreach
Avec une itération manuelle
Implémentation avec ConfigureAwait(false)
Utiliser un CancellationToken
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:
IAsyncEnumerable<T>.GetAsyncEnumerator()
pour instancier l’énumérateur,IAsyncEnumerator<T>.Current
pour obtenir le résultat de l’itération,IAsyncEnumerator<T>.MoveNextAsync()
pour effectuer une nouvelle itération etIAsyncEnumerator<T>.DisposeAsync()
pour arrêter l’énumération.
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(); // ...
- Using declarations: https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-8#using-declarations
- Mot-clé yield: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/yield
- C# 8 features – part 2 (async method with yield return): https://agirlamonggeeks.com/2018/11/25/c-8-features-part-2-async-method-with-yield-return/
- .NET Futures: Asynchronous Streams: https://www.infoq.com/news/2017/05/async-streams/
- Iterating with Async Enumerables in C# 8: https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8
- .NET Asynchronous Disposal – Tips for Implementing IAsyncDisposable on your own Types: https://alistairevans.co.uk/2019/10/24/net-asynchronous-disposal-tips-for-implementing-iasyncdisposable-on-your-own-types/