Note
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier les répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de changer de répertoire.
Si votre code implémente des scénarios liés aux E/S pour prendre en charge les demandes de données réseau, l’accès à la base de données ou les écritures de système de fichiers, la programmation asynchrone est la meilleure approche. Vous pouvez également écrire du code asynchrone pour des scénarios liés au processeur, comme des calculs coûteux.
C# a un modèle de programmation asynchrone au niveau du langage qui vous permet d’écrire facilement du code asynchrone sans avoir à jugler les rappels ou à se conformer à une bibliothèque qui prend en charge l’asynchronie. Le modèle suit ce qu’on appelle le modèle asynchrone basé sur les tâches (TAP).
Explorer le modèle de programmation asynchrone
Les objets Task et Task<T> représentent le cœur de la programmation asynchrone. Ces objets sont utilisés pour modéliser des opérations asynchrones en prenant en charge les mots clés async et await. Dans la plupart des cas, le modèle est assez simple pour les scénarios liés aux E/S et au processeur. A l’intérieur d’une méthode async :
- Le code lié aux E/S démarre une opération représentée par un
Taskou unTask<T>objet dans la méthodeasync. - Le code lié au processeur démarre une opération sur un thread d’arrière-plan avec la Task.Run méthode.
Dans les deux cas, un actif Task représente une opération asynchrone qui peut ne pas être terminée.
Le mot clé await trouve ici toute son utilité. Il génère un contrôle à l’appelant de la méthode qui contient l’expression await , et permet finalement à l’interface utilisateur d’être réactive ou d’un service d’être élastique. Bien qu’il existe des façons d’aborder le code asynchrone autres que par l’utilisation des expressions async et await, cet article se concentre sur les constructions au niveau du langage.
Remarque
Certains exemples présentés dans cet article utilisent la System.Net.Http.HttpClient classe pour télécharger des données à partir d’un service web. Dans l’exemple de code, l’objet s_httpClient est un champ statique de classe de type Program :
private static readonly HttpClient s_httpClient = new();
Pour plus d’informations, consultez l’exemple de code complet à la fin de cet article.
Passer en revue les concepts sous-jacents
Lorsque vous implémentez la programmation asynchrone dans votre code C#, le compilateur transforme votre programme en ordinateur d’état. Cette construction effectue le suivi de différentes opérations et états dans votre code, telles que la suspension de l’exécution lorsque le code atteint une expression await et la reprise de l’exécution lorsqu’un travail en arrière-plan se termine.
En termes de théorie de la science informatique, la programmation asynchrone est une implémentation du modèle Promise d’asynchronie.
Dans le modèle de programmation asynchrone, il existe plusieurs concepts clés à comprendre :
- Vous pouvez utiliser du code asynchrone pour le code lié aux E/S et le code lié au processeur, mais l’implémentation est différente.
- Le code asynchrone utilise les objets
Task<T>etTasken tant que constructions pour modéliser le travail en cours d’exécution en arrière-plan. - Le
asyncmot clé déclare une méthode en tant que méthode asynchrone, ce qui vous permet d’utiliser leawaitmot clé dans le corps de la méthode. - Lorsque vous appliquez le
awaitmot clé, le code suspend la méthode appelante et retourne le contrôle à son appelant jusqu’à ce que la tâche se termine. - Vous ne pouvez utiliser l’expression
awaitque dans une méthode asynchrone.
Exemple lié aux E/S : Télécharger des données à partir du service web
Dans cet exemple, lorsque l’utilisateur sélectionne un bouton, l’application télécharge les données à partir d’un service web. Vous ne souhaitez pas bloquer le thread d’interface utilisateur de l’application pendant le processus de téléchargement. Le code suivant effectue cette tâche :
s_downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await s_httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
Le code exprime l’intention (télécharger des données de façon asynchrone) sans que cela nécessite des interactions compliquées avec les objets Task.
Exemple lié au processeur : Exécuter le calcul du jeu
Dans l’exemple suivant, un jeu mobile inflige des dommages à plusieurs agents sur l’écran en réponse à un événement de bouton. L’exécution du calcul des dommages peut être coûteuse. L’exécution du calcul sur le thread d’interface utilisateur peut entraîner des problèmes d’affichage et d’interaction de l’interface utilisateur pendant le calcul.
La meilleure façon de gérer la tâche consiste à démarrer un thread en arrière-plan pour terminer le travail avec la méthode Task.Run. L’opération se met en pause à l’aide d’une expression await. L’opération reprend une fois la tâche terminée. Cette approche permet à l’interface utilisateur de s’exécuter correctement pendant que le travail se termine en arrière-plan.
static DamageResult CalculateDamageDone()
{
return new DamageResult()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
};
}
s_calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
Le code exprime clairement l’intention de l’événement de bouton Clicked . Il ne nécessite pas de gestion manuelle d’un thread d’arrière-plan et termine la tâche de manière non bloquante.
Reconnaître les scénarios liés au processeur et aux E/S
Les exemples précédents montrent comment utiliser le modificateur async et l’expression await pour le travail lié aux E/S et au travail lié au processeur. Un exemple pour chaque scénario montre comment le code est différent en fonction de l’emplacement où l’opération est liée. Pour préparer votre implémentation, vous devez comprendre comment identifier quand une opération est liée à des E/S ou liées au processeur. Votre choix d’implémentation peut considérablement affecter les performances de votre code et entraîner une mauvaise utilisation des constructions.
Il existe deux questions principales à résoudre avant d’écrire du code :
| Question | Scénario | Implémentation |
|---|---|---|
| Le code doit-il attendre un résultat ou une action, comme les données d’une base de données ? | Lié aux E/S | Utilisez le modificateur async et l’expression awaitsans la méthode Task.Run. Évitez d’utiliser la bibliothèque parallèle de tâches. |
| Le code doit-il exécuter un calcul coûteux ? | Limité par le processeur | Utilisez le modificateur async et l'expression await, mais déléguez le travail sur un autre fil d'exécution avec la méthode Task.Run. Cette approche traite des problèmes liés à la réactivité du processeur. Si le travail est approprié pour la concurrence et le parallélisme, envisagez également d’utiliser la bibliothèque parallèle de tâches. |
Mesurez toujours l’exécution de votre code. Vous pouvez découvrir que votre travail lié au processeur n’est pas suffisamment coûteux par rapport à la surcharge des commutateurs de contexte lors de la multithreading. Chaque choix a des compromis. Choisissez le compromis approprié pour votre situation.
Explorer d’autres exemples
Les exemples de cette section illustrent plusieurs façons d’écrire du code asynchrone en C#. Ils couvrent quelques scénarios que vous pouvez rencontrer.
Extraire des données d’un réseau
Le code suivant télécharge le code HTML à partir d’une URL donnée et compte le nombre de fois où la chaîne .NET se produit dans le code HTML. Le code utilise ASP.NET pour définir une méthode de contrôleur d’API web, qui effectue la tâche et retourne le nombre.
Remarque
Si vous prévoyez d’effectuer une analyse HTML dans le code de production, n’utilisez pas d’expressions régulières. Utilisez plutôt une bibliothèque d’analyse.
[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCountAsync(string URL)
{
// Suspends GetDotNetCountAsync() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await s_httpClient.GetStringAsync(URL);
return Regex.Matches(html, @"\.NET").Count;
}
Vous pouvez écrire du code similaire pour une application Windows universelle et effectuer la tâche de comptage après une pression sur un bouton :
private readonly HttpClient _httpClient = new HttpClient();
private async void OnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)
{
// Capture the task handle here so we can await the background task later.
var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");
// Any other work on the UI thread can be done here, such as enabling a Progress Bar.
// It's important to do the extra work here before the "await" call,
// so the user sees the progress bar before execution of this method is yielded.
NetworkProgressBar.IsEnabled = true;
NetworkProgressBar.Visibility = Visibility.Visible;
// The await operator suspends OnSeeTheDotNetsButtonClick(), returning control to its caller.
// This action is what allows the app to be responsive and not block the UI thread.
var html = await getDotNetFoundationHtmlTask;
int count = Regex.Matches(html, @"\.NET").Count;
DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";
NetworkProgressBar.IsEnabled = false;
NetworkProgressBar.Visibility = Visibility.Collapsed;
}
Attendre la fin de plusieurs tâches
Dans certains scénarios, le code doit récupérer plusieurs éléments de données simultanément. Les Task API fournissent des méthodes qui vous permettent d’écrire du code asynchrone qui effectue une attente non bloquante sur plusieurs travaux en arrière-plan :
- méthode Task.WhenAll
- méthode Task.WhenAny
L’exemple suivant montre comment récupérer des données de l'objet User pour un ensemble d’objets userId.
private static async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
return await Task.FromResult(new User() { id = userId });
}
private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}
return await Task.WhenAll(getUserTasks);
}
Vous pouvez écrire ce code plus succinctement à l’aide de LINQ :
private static async Task<User[]> GetUsersByLINQAsync(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
Bien que vous écriviez moins de code à l’aide de LINQ, soyez prudent lors du mélange de LINQ avec du code asynchrone. LINQ utilise l’exécution différée (ou différée), ce qui signifie que sans évaluation immédiate, les appels asynchrones ne se produisent pas tant que la séquence n’est pas énumérée.
L’exemple précédent est correct et sûr, car il utilise la méthode pour évaluer immédiatement la Enumerable.ToArray requête LINQ et stocker les tâches dans un tableau. Cette approche garantit que les id => GetUserAsync(id) appels s’exécutent immédiatement et que toutes les tâches démarrent simultanément, tout comme l’approche de foreach boucle.
Enumerable.ToArray Utilisez toujours ou Enumerable.ToList lors de la création de tâches avec LINQ pour garantir l’exécution immédiate et l’exécution simultanée des tâches. Voici un exemple illustrant l'utilisation de ToList() avec Task.WhenAny pour traiter les tâches au fur et à mesure qu'elles se terminent :
private static async Task ProcessTasksAsTheyCompleteAsync(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToList();
while (getUserTasks.Count > 0)
{
Task<User> completedTask = await Task.WhenAny(getUserTasks);
getUserTasks.Remove(completedTask);
User user = await completedTask;
Console.WriteLine($"Processed user {user.id}");
}
}
Dans cet exemple, ToList() crée une liste qui prend en charge l’opération Remove() , ce qui vous permet de supprimer dynamiquement les tâches terminées. Ce modèle est particulièrement utile lorsque vous souhaitez gérer les résultats dès qu’ils sont disponibles, plutôt que d’attendre que toutes les tâches se terminent.
Bien que vous écriviez moins de code à l’aide de LINQ, soyez prudent lors du mélange de LINQ avec du code asynchrone. LINQ utilise l'exécution différée (ou évaluation paresseuse). Les appels asynchrones ne se produisent pas immédiatement comme c'est le cas dans une boucle foreach, sauf si vous forcez la séquence générée à itérer par un appel à la méthode .ToList() ou la méthode .ToArray().
Vous pouvez choisir entre Enumerable.ToArray et Enumerable.ToList en fonction de votre scénario :
- Utilisez
ToArray()lorsque vous prévoyez de traiter toutes les tâches ensemble, comme avecTask.WhenAll. Les tableaux sont efficaces pour les scénarios où la taille de collection est fixe. - Utilisez
ToList()lorsque vous devez gérer dynamiquement des tâches, comme avecTask.WhenAny, où vous pourriez retirer les tâches terminées de la collection une fois qu'elles sont achevées.
Examiner les considérations relatives à la programmation asynchrone
Avec la programmation asynchrone, il existe plusieurs détails à garder à l’esprit qui peuvent empêcher un comportement inattendu.
Utiliser await dans le corps de la méthode async()
Lorsque vous utilisez le async modificateur, vous devez inclure une ou plusieurs await expressions dans le corps de la méthode. Si le compilateur ne rencontre pas d’expression await , la méthode ne parvient pas à générer. Bien que le compilateur génère un avertissement, le code se compile toujours et le compilateur exécute la méthode. L’ordinateur d’état généré par le compilateur C# pour la méthode asynchrone n’accomplit rien, de sorte que l’ensemble du processus est très inefficace.
Ajouter le suffixe « Async » aux noms de méthodes asynchrones
La convention de style .NET consiste à ajouter le suffixe « Async » à tous les noms de méthode asynchrones. Cette approche permet de différencier plus facilement les méthodes synchrones et asynchrones. Certaines méthodes qui ne sont pas explicitement appelées par votre code (telles que les gestionnaires d’événements ou les méthodes de contrôleur web) ne s’appliquent pas nécessairement dans ce scénario. Étant donné que ces éléments ne sont pas explicitement appelés par votre code, l’utilisation d’un nommage explicite n’est pas aussi importante.
Retourne « async void » uniquement pour les gestionnaires d’événements
Les gestionnaires d’événements doivent déclarer des types de retour void et ne peuvent pas utiliser ou retourner des objets Task et Task<T> comme le font d’autres méthodes. Lorsque vous écrivez des gestionnaires d’événements asynchrones, vous devez utiliser le async modificateur sur une void méthode de retour pour les gestionnaires. D'autres implémentations de méthodes de retour de async void ne suivent pas le modèle TAP et peuvent poser des problèmes.
- Les exceptions lancées dans une méthode
async voidne peuvent pas être rattrapées en dehors de cette méthode. -
async voidles méthodes sont difficiles à tester -
async voidles méthodes peuvent provoquer des effets secondaires négatifs si l’appelant ne s’attend pas à ce qu’ils soient asynchrones
Utilisez une prudence avec des lambda asynchrones dans LINQ
Il est important d’utiliser la prudence lorsque vous implémentez des lambda asynchrones dans des expressions LINQ. Les expressions lambda dans LINQ utilisent l’exécution différée, ce qui signifie que le code peut s’exécuter à un moment inattendu. L’introduction de tâches bloquantes dans ce scénario peut facilement entraîner un blocage, si le code n’est pas écrit correctement. En outre, l’imbrication du code asynchrone peut également rendre difficile le raisonnement sur l’exécution du code. Async et LINQ sont puissants, mais ces techniques doivent être utilisées ensemble aussi soigneusement et clairement que possible.
Rendement pour les tâches de manière non bloquante
Si votre programme a besoin du résultat d’une tâche, écrivez du code qui implémente l’expression await de manière non bloquante. Le blocage du thread actuel comme moyen d’attendre de façon synchrone pour qu’un Task élément se termine peut entraîner des blocages et des threads de contexte bloqués. Cette approche de programmation peut nécessiter une gestion des erreurs plus complexe. Le tableau suivant fournit des conseils sur la façon d'accéder aux résultats des tâches de manière non bloquante.
| Scénario de tâche | Code actuel | Remplacer par « await » |
|---|---|---|
| Récupérer le résultat d’une tâche en arrière-plan |
Task.Wait ou Task.Result |
await |
| Continuer lorsque n'importe quelle tâche est terminée | Task.WaitAny |
await Task.WhenAny |
| Continuer lorsque toutes les tâches se terminent | Task.WaitAll |
await Task.WhenAll |
| Continuer après un certain temps | Thread.Sleep |
await Task.Delay |
Envisagez d’utiliser le type ValueTask
Lorsqu’une méthode asynchrone renvoie un objet Task, des goulets d’étranglement peuvent apparaître dans certains chemins. Étant donné que Task est un type de référence, un objet Task est alloué à partir du heap. Si une méthode déclarée avec le async modificateur retourne un résultat mis en cache ou se termine de façon synchrone, les allocations supplémentaires peuvent accumuler des coûts de temps significatifs dans les sections critiques de performances du code. Ce scénario peut devenir coûteux lorsque les allocations se produisent dans des boucles serrées. Pour plus d’informations, consultez Types de retour asynchrones généralisés.
Comprendre quand définir ConfigureAwait(false)
Les développeurs se demandent souvent quand utiliser la Task.ConfigureAwait(Boolean) valeur booléenne. Cette API permet à une Task instance de configurer le contexte de l’ordinateur d’état qui implémente n’importe quelle await expression. Lorsque la valeur booléenne n’est pas définie correctement, les performances peuvent se dégrader ou provoquer des verrouillages. Pour plus d’informations, consultez la FAQ ConfigureAwait.
Écrire un code sans état
Évitez d’écrire du code qui dépend de l’état des objets globaux ou de l’exécution de certaines méthodes. Le code doit uniquement dépendre des valeurs de retour des méthodes. Il y a de nombreux avantages à écrire du code sans état :
- Plus facile à raisonner sur le code
- Plus facile à tester le code
- Plus simple pour combiner du code asynchrone et synchrone
- Capable d’éviter les conditions de concurrence dans le code
- Code asynchrone simple à coordonner qui dépend des valeurs de retour
- (Bonus) Fonctionne bien avec l’injection de dépendances dans le code
L’objectif recommandé est d’atteindre une transparence référentielle complète ou quasi-complète dans votre code. Cette approche entraîne une base de code prévisible, testable et maintenable.
Accès synchrone aux opérations asynchrones
Dans les scénarios, vous devrez peut-être bloquer les opérations asynchrones lorsque le await mot clé n’est pas disponible dans votre pile d’appels. Cette situation se produit dans les bases de code héritées ou lors de l’intégration de méthodes asynchrones dans des API synchrones qui ne peuvent pas être modifiées.
Avertissement
Le blocage synchrone sur les opérations asynchrones peut entraîner des blocages et doit être évité dans la mesure du possible. La solution préférée consiste à utiliser async/await tout au long de votre pile d’appels.
Lorsque vous devez bloquer de manière synchrone sur un Task, voici les approches disponibles, répertoriées de la plupart aux moins recommandées :
- Utilisez GetAwaiter(). GetResult()
- Utiliser Task.Run pour des scénarios complexes
- Utiliser Wait() et Result
Utilisez GetAwaiter(). GetResult()
Le GetAwaiter().GetResult() modèle est généralement l’approche recommandée lorsque vous devez bloquer de façon synchrone :
// When you cannot use await
Task<string> task = GetDataAsync();
string result = task.GetAwaiter().GetResult();
Cette approche :
- Conserve l’exception d’origine sans l’encapsuler dans un
AggregateException. - Bloque le thread actuel jusqu’à ce que la tâche se termine.
- Il y a toujours un risque d’interblocage s’il n’est pas utilisé soigneusement.
Utiliser Task.Run pour des scénarios complexes
Pour les scénarios complexes où vous devez isoler le travail asynchrone :
// Offload to thread pool to avoid context deadlocks
string result = Task.Run(async () => await GetDataAsync()).GetAwaiter().GetResult();
Ce modèle :
- Exécute la méthode asynchrone sur un thread du pool.
- Peut vous aider à éviter certains scénarios d’interblocage.
- Ajoute une surcharge en programmant le travail dans le pool de threads.
Utilisez Wait() et Résultat
Vous pouvez utiliser une approche bloquante en appelant Wait() et Result. Toutefois, cette approche est déconseillée, car elle encapsule les exceptions AggregateException.
Task<string> task = GetDataAsync();
task.Wait();
string result = task.Result;
Problèmes avec Wait() et Result:
- Les exceptions sont encapsulées,
AggregateExceptionce qui rend la gestion des erreurs plus complexe. - Risque d’interblocage plus élevé.
- Intention moins claire dans le code.
Considérations supplémentaires
- Prévention du blocage : soyez particulièrement prudent dans les applications d’interface utilisateur ou lors de l’utilisation d’un contexte de synchronisation.
- Impact sur les performances : le blocage des threads réduit l’extensibilité.
- Gestion des exceptions : testez soigneusement les scénarios d’erreur, car le comportement d’exception diffère entre les modèles.
Pour obtenir des instructions plus détaillées sur les défis et les considérations relatives aux wrappers synchrones pour les méthodes asynchrones, voir Dois-je exposer des wrappers synchrones pour les méthodes asynchrones ?.
Passez en revue l’exemple complet
Le code suivant représente l’exemple complet, qui est disponible dans l’exemple de fichier Program.cs .
using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;
class Button
{
public Func<object, object, Task>? Clicked
{
get;
internal set;
}
}
class DamageResult
{
public int Damage
{
get { return 0; }
}
}
class User
{
public bool isEnabled
{
get;
set;
}
public int id
{
get;
set;
}
}
public class Program
{
private static readonly Button s_downloadButton = new();
private static readonly Button s_calculateButton = new();
private static readonly HttpClient s_httpClient = new();
private static readonly IEnumerable<string> s_urlList = new string[]
{
"https://learn.microsoft.com",
"https://learn.microsoft.com/aspnet/core",
"https://learn.microsoft.com/azure",
"https://learn.microsoft.com/azure/devops",
"https://learn.microsoft.com/dotnet",
"https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio",
"https://learn.microsoft.com/education",
"https://learn.microsoft.com/shows/net-core-101/what-is-net",
"https://learn.microsoft.com/enterprise-mobility-security",
"https://learn.microsoft.com/gaming",
"https://learn.microsoft.com/graph",
"https://learn.microsoft.com/microsoft-365",
"https://learn.microsoft.com/office",
"https://learn.microsoft.com/powershell",
"https://learn.microsoft.com/sql",
"https://learn.microsoft.com/surface",
"https://dotnetfoundation.org",
"https://learn.microsoft.com/visualstudio",
"https://learn.microsoft.com/windows",
"https://learn.microsoft.com/maui"
};
private static void Calculate()
{
static DamageResult CalculateDamageDone()
{
return new DamageResult()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
};
}
s_calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
}
private static void DisplayDamage(DamageResult damage)
{
Console.WriteLine(damage.Damage);
}
private static void Download(string URL)
{
s_downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await s_httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
}
private static void DoSomethingWithData(object stringData)
{
Console.WriteLine($"Displaying data: {stringData}");
}
private static async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
return await Task.FromResult(new User() { id = userId });
}
private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}
return await Task.WhenAll(getUserTasks);
}
private static async Task<User[]> GetUsersByLINQAsync(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
private static async Task ProcessTasksAsTheyCompleteAsync(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToList();
while (getUserTasks.Count > 0)
{
Task<User> completedTask = await Task.WhenAny(getUserTasks);
getUserTasks.Remove(completedTask);
User user = await completedTask;
Console.WriteLine($"Processed user {user.id}");
}
}
[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCountAsync(string URL)
{
// Suspends GetDotNetCountAsync() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await s_httpClient.GetStringAsync(URL);
return Regex.Matches(html, @"\.NET").Count;
}
static async Task Main()
{
Console.WriteLine("Application started.");
Console.WriteLine("Counting '.NET' phrase in websites...");
int total = 0;
foreach (string url in s_urlList)
{
var result = await GetDotNetCountAsync(url);
Console.WriteLine($"{url}: {result}");
total += result;
}
Console.WriteLine("Total: " + total);
Console.WriteLine("Retrieving User objects with list of IDs...");
IEnumerable<int> ids = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
var users = await GetUsersAsync(ids);
foreach (User? user in users)
{
Console.WriteLine($"{user.id}: isEnabled={user.isEnabled}");
}
Console.WriteLine("Processing tasks as they complete...");
await ProcessTasksAsTheyCompleteAsync(ids);
Console.WriteLine("Application ending.");
}
}
// Example output:
//
// Application started.
// Counting '.NET' phrase in websites...
// https://learn.microsoft.com: 0
// https://learn.microsoft.com/aspnet/core: 57
// https://learn.microsoft.com/azure: 1
// https://learn.microsoft.com/azure/devops: 2
// https://learn.microsoft.com/dotnet: 83
// https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio: 31
// https://learn.microsoft.com/education: 0
// https://learn.microsoft.com/shows/net-core-101/what-is-net: 42
// https://learn.microsoft.com/enterprise-mobility-security: 0
// https://learn.microsoft.com/gaming: 0
// https://learn.microsoft.com/graph: 0
// https://learn.microsoft.com/microsoft-365: 0
// https://learn.microsoft.com/office: 0
// https://learn.microsoft.com/powershell: 0
// https://learn.microsoft.com/sql: 0
// https://learn.microsoft.com/surface: 0
// https://dotnetfoundation.org: 16
// https://learn.microsoft.com/visualstudio: 0
// https://learn.microsoft.com/windows: 0
// https://learn.microsoft.com/maui: 6
// Total: 238
// Retrieving User objects with list of IDs...
// 1: isEnabled= False
// 2: isEnabled= False
// 3: isEnabled= False
// 4: isEnabled= False
// 5: isEnabled= False
// 6: isEnabled= False
// 7: isEnabled= False
// 8: isEnabled= False
// 9: isEnabled= False
// 0: isEnabled= False
// Application ending.