Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Lorsque vous utilisez le modèle asynchrone basé sur des tâches (TAP) pour travailler avec des opérations asynchrones, vous pouvez utiliser des rappels pour attendre sans bloquer. Pour les tâches, cela s’effectue par le biais de méthodes telles que Task.ContinueWith. La prise en charge asynchrone basée sur le langage masque les rappels en permettant aux opérations asynchrones d’être attendues dans le flux de contrôle normal, et le code généré par le compilateur fournit cette même prise en charge au niveau de l’API.
Suspension de l'exécution en utilisant Await
Vous pouvez utiliser le mot clé Await en C# et l’opérateur Await en Visual Basic pour attendre Task et Task<TResult> objets de manière asynchrone. Lorsque vous attendez un Task, l’expression await
est de type void
. Lorsque vous attendez un Task<TResult>, l’expression await
est de type TResult
. Une await
expression doit apparaître à l’intérieur du corps d’une méthode asynchrone. (Ces fonctionnalités de langage ont été introduites dans .NET Framework 4.5.)
En arrière-plan, la fonctionnalité await installe un rappel sur la tâche à l’aide d’une continuation. Ce rappel reprend la méthode asynchrone au point d’interruption. Quand la méthode asynchrone est reprise, si l’opération attendue s’est terminée correctement et est une Task<TResult>, son TResult
est retourné. Si la Task ou Task<TResult> attendue s’est terminée dans l’état Canceled, une exception OperationCanceledException est levée. Si la Task ou Task<TResult> attendue s’est terminée dans l’état Faulted, l’exception qui a entraîné l’erreur est levée. Une Task
peut échouer à cause de plusieurs exceptions, mais seule une de ces exceptions est propagée. Toutefois, la Task.Exception propriété retourne une AggregateException exception qui contient toutes les erreurs.
Si un contexte de synchronisation (SynchronizationContext objet) est associé au thread qui exécutait la méthode asynchrone au moment de la suspension (par exemple, si la SynchronizationContext.Current propriété n’est pas null
), la méthode asynchrone reprend sur ce même contexte de synchronisation à l’aide de la méthode du Post contexte. Dans le cas contraire, il s’appuie sur le planificateur de tâches (TaskScheduler objet) qui était en cours au moment de la suspension. En règle générale, il s’agit du planificateur de tâches par défaut (TaskScheduler.Default), qui cible le pool de threads. Ce planificateur de tâches détermine si l’opération asynchrone attendue doit reprendre là où elle s'est terminée ou si la reprise doit être planifiée. Le planificateur par défaut permet généralement à la continuation de s’exécuter sur le thread qui a effectué l’opération attendue.
Lorsqu’une méthode asynchrone est appelée, elle exécute simultanément le corps de la fonction jusqu'à la première expression await sur une instance prenant en charge await qui n’est pas encore terminée. À ce moment, l’appel retourne à l’appelant. Si la méthode asynchrone ne retourne pas void
, un objet Task ou Task<TResult> est retourné pour représenter le calcul en cours. Dans une méthode asynchrone non void, si une instruction return est rencontrée ou que la fin du corps de la méthode est atteinte, la tâche est terminée dans l’état RanToCompletion final. Si une exception non gérée cause la sortie du contrôle du corps de la méthode asynchrone, la tâche se termine dans l’état Faulted. Si cette exception est un OperationCanceledException, la tâche se termine à la place dans l’état Canceled . De cette façon, le résultat ou l’exception est finalement publié.
Il existe plusieurs variantes importantes de ce comportement. Pour des raisons de performances, si une tâche est déjà terminée au moment où la tâche est attendue, le contrôle n’est pas cédé et la fonction continue d’exécuter. En outre, le retour au contexte d’origine n’est pas toujours le comportement souhaité et peut être modifié ; ceci est décrit plus en détail dans la section suivante.
Configuration de la suspension et de la reprise avec Yield et ConfigureAwait
Plusieurs méthodes fournissent davantage de contrôle sur l’exécution d’une méthode asynchrone. Par exemple, vous pouvez utiliser la Task.Yield méthode pour introduire un point de rendement dans la méthode asynchrone :
public class Task : …
{
public static YieldAwaitable Yield();
…
}
Cela équivaut à la publication asynchrone ou à la replanification vers le contexte actuel.
Task.Run(async delegate
{
for(int i=0; i<1000000; i++)
{
await Task.Yield(); // fork the continuation into a separate work item
...
}
});
Vous pouvez également utiliser la Task.ConfigureAwait méthode pour mieux contrôler la suspension et la reprise dans une méthode asynchrone. Comme mentionné précédemment, par défaut, le contexte actuel est capturé au moment où une méthode asynchrone est suspendue, et ce contexte capturé est utilisé pour appeler la continuation de la méthode asynchrone lors de la reprise. Dans de nombreux cas, il s’agit du comportement exact souhaité. Dans certains cas, vous ne vous souciez peut-être pas du contexte de continuation et vous pouvez obtenir de meilleures performances en évitant de revenir au contexte d'origine. Pour ce faire, utilisez la méthode Task.ConfigureAwait pour informer l’opération await de ne pas capturer et reprendre sur le contexte, mais plutôt de poursuivre l’exécution quand l’opération asynchrone attendue est terminée :
await someTask.ConfigureAwait(continueOnCapturedContext:false);
Annulation d’une opération asynchrone
À compter de .NET Framework 4, les méthodes TAP qui prennent en charge l’annulation fournissent au moins une surcharge qui accepte un jeton d’annulation (CancellationToken objet).
Un jeton d’annulation est créé via une source de jeton d’annulation (CancellationTokenSource objet). La propriété de la source Token retourne le jeton d’annulation qui sera signalé lorsque la méthode Cancel de la source est appelée. Par exemple, si vous souhaitez télécharger une page web unique et que vous souhaitez pouvoir annuler l’opération, vous créez un CancellationTokenSource objet, transmettez son jeton à la méthode TAP, puis appelez la méthode de Cancel la source lorsque vous êtes prêt à annuler l’opération :
var cts = new CancellationTokenSource();
string result = await DownloadStringTaskAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.Cancel();
Pour annuler plusieurs appels asynchrones, vous pouvez passer le même jeton à tous les appels :
var cts = new CancellationTokenSource();
IList<string> results = await Task.WhenAll(from url in urls select DownloadStringTaskAsync(url, cts.Token));
// at some point later, potentially on another thread
…
cts.Cancel();
Vous pouvez également passer le même jeton à un sous-ensemble sélectif d’opérations :
var cts = new CancellationTokenSource();
byte [] data = await DownloadDataAsync(url, cts.Token);
await SaveToDiskAsync(outputPath, data, CancellationToken.None);
… // at some point later, potentially on another thread
cts.Cancel();
Importante
Les demandes d’annulation peuvent être lancées à partir de n’importe quel thread.
Vous pouvez transmettre la CancellationToken.None valeur à n’importe quelle méthode qui accepte un jeton d’annulation pour indiquer que l’annulation ne sera jamais demandée. Cela entraîne la propriété CancellationToken.CanBeCanceled à renvoyer false
, et la méthode appelée peut optimiser en conséquence. À des fins de test, vous pouvez également transmettre un jeton d’annulation pré-annulé instancié à l’aide du constructeur qui accepte une valeur booléenne pour indiquer si le jeton doit démarrer dans un état déjà annulé ou non annulable.
Cette approche de l’annulation présente plusieurs avantages :
Vous pouvez transmettre le même jeton d’annulation à n’importe quel nombre d’opérations asynchrones et synchrones.
La même demande d’annulation peut être propagée à un nombre quelconque de destinataires.
Le développeur de l’API asynchrone contrôle complètement si l’annulation peut être demandée et quand elle peut prendre effet.
Le code qui consomme l’API peut déterminer de manière sélective les appels asynchrones auxquels les demandes d’annulation seront propagées.
Surveillance de la progression
Certaines méthodes asynchrones exposent la progression via une interface de progression transmise à la méthode asynchrone. Par exemple, considérez une fonction qui télécharge de façon asynchrone une chaîne de texte, et le long de la route déclenche des mises à jour de progression qui incluent le pourcentage du téléchargement terminé jusqu’à présent. Une telle méthode peut être consommée dans une application WPF (Windows Presentation Foundation) comme suit :
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.IsEnabled = false;
try
{
txtResult.Text = await DownloadStringTaskAsync(txtUrl.Text,
new Progress<int>(p => pbDownloadProgress.Value = p));
}
finally { btnDownload.IsEnabled = true; }
}
Utilisation des combinateurs intégrés basés sur des tâches
L’espace de noms System.Threading.Tasks inclut plusieurs méthodes de composition et d’utilisation des tâches.
Task.Run
La Task classe inclut plusieurs Run méthodes qui vous permettent de décharger facilement le travail vers le pool de threads en tant que Task ou Task<TResult>, par exemple :
public async void button1_Click(object sender, EventArgs e)
{
textBox1.Text = await Task.Run(() =>
{
// … do compute-bound work here
return answer;
});
}
Certaines de ces Run méthodes, telles que la Task.Run(Func<Task>) surcharge, existent comme raccourcis pour la TaskFactory.StartNew méthode. Cette surcharge vous permet d’utiliser await dans le travail déchargé, par exemple :
public async void button1_Click(object sender, EventArgs e)
{
pictureBox1.Image = await Task.Run(async() =>
{
using(Bitmap bmp1 = await DownloadFirstImageAsync())
using(Bitmap bmp2 = await DownloadSecondImageAsync())
return Mashup(bmp1, bmp2);
});
}
Ces surcharges sont logiquement équivalentes à l’utilisation de la méthode TaskFactory.StartNew conjointement avec la méthode d’extension Unwrap dans la bibliothèque de tâches parallèles.
Task.FromResult
Utilisez la méthode FromResult dans les scénarios où les données peuvent déjà être disponibles et doivent simplement être renvoyées par une méthode retournant des tâches intégrée dans un Task<TResult>.
public Task<int> GetValueAsync(string key)
{
int cachedValue;
return TryGetCachedValue(out cachedValue) ?
Task.FromResult(cachedValue) :
GetValueAsyncInternal();
}
private async Task<int> GetValueAsyncInternal(string key)
{
…
}
Task.WhenAll
Utilisez la WhenAll méthode pour attendre de façon asynchrone plusieurs opérations asynchrones représentées en tant que tâches. La méthode a plusieurs surcharges qui prennent en charge un ensemble de tâches non génériques ou un ensemble non uniforme de tâches génériques (par exemple, en attente asynchrone de plusieurs opérations de retour vide ou en attente asynchrone de plusieurs méthodes de retour de valeur où chaque valeur peut avoir un type différent) et pour prendre en charge un ensemble uniforme de tâches génériques (par exemple, en attendant de manière asynchrone plusieurs TResult
méthodes de retour de valeurs).
Supposons que vous souhaitez envoyer des messages électroniques à plusieurs clients. Vous pouvez chevaucher l’envoi des messages afin que vous n’attendiez pas qu’un message se termine avant d’envoyer le suivant. Vous pouvez également déterminer quand les opérations d’envoi se sont terminées et si des erreurs se sont produites :
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);
Ce code ne gère pas explicitement les exceptions qui peuvent se produire, mais laisse les exceptions propager le await
sur la tâche obtenue à partir de WhenAll. Pour gérer les exceptions, vous pouvez utiliser du code comme suit :
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
try
{
await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
...
}
Dans ce cas, si une opération asynchrone échoue, toutes les exceptions sont consolidées dans une AggregateException exception, qui est stockée dans l’élément Task retourné par la WhenAll méthode. Toutefois, une seule de ces exceptions est propagée par le await
mot clé. Si vous souhaitez examiner toutes les exceptions, vous pouvez réécrire le code précédent comme suit :
Task [] asyncOps = (from addr in addrs select SendMailAsync(addr)).ToArray();
try
{
await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
foreach(Task faulted in asyncOps.Where(t => t.IsFaulted))
{
… // work with faulted and faulted.Exception
}
}
Prenons un exemple de téléchargement asynchrone de plusieurs fichiers à partir du web. Dans ce cas, toutes les opérations asynchrones ont des types de résultats homogènes et il est facile d’accéder aux résultats :
string [] pages = await Task.WhenAll(
from url in urls select DownloadStringTaskAsync(url));
Vous pouvez utiliser les mêmes techniques de gestion des exceptions que celles décrites dans le scénario précédent de retour vide :
Task<string> [] asyncOps =
(from url in urls select DownloadStringTaskAsync(url)).ToArray();
try
{
string [] pages = await Task.WhenAll(asyncOps);
...
}
catch(Exception exc)
{
foreach(Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
{
… // work with faulted and faulted.Exception
}
}
Task.WhenAny
Vous pouvez utiliser la méthode WhenAny pour attendre de façon asynchrone qu’une seule des multiples opérations asynchrones représentées en tant que tâches soit effectuée. Cette méthode sert quatre cas d’usage principaux :
Redondance : exécution d’une opération plusieurs fois et sélection de celle qui se termine en premier (par exemple, en contactant plusieurs services web de devis boursiers qui produisent un résultat unique et en sélectionnant celui qui se termine le plus rapidement).
Entrelacement : lancement de plusieurs opérations et attente de leur achèvement, mais traitement de celles-ci au fur et à mesure qu’elles se terminent.
Limitation : autorisation de démarrer des opérations supplémentaires une fois que d’autres sont terminées. Il s’agit d’une extension de l’entrelacement.
Sauvetage anticipé : par exemple, une opération représentée par la tâche t1 peut être regroupée dans une tâche WhenAny avec une autre tâche t2, et vous pouvez attendre la tâche WhenAny. La tâche t2 peut représenter un délai d’attente ou une annulation, ou un autre signal qui entraîne la fin de la WhenAny tâche avant la fin de la tâche t1.
Redondance
Considérez un cas où vous souhaitez prendre une décision sur l’achat d’un stock. Il existe plusieurs services web de recommandation de stock que vous approuvez, mais en fonction de la charge quotidienne, chaque service peut finir par être lent à des moments différents. Vous pouvez utiliser la WhenAny méthode pour recevoir une notification quand une opération se termine :
var recommendations = new List<Task<bool>>()
{
GetBuyRecommendation1Async(symbol),
GetBuyRecommendation2Async(symbol),
GetBuyRecommendation3Async(symbol)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
if (await recommendation) BuyStock(symbol);
Contrairement à WhenAll, qui retourne les résultats directs de toutes les tâches qui ont été menées à bien avec succès, WhenAny retourne la tâche qui a été complétée. Si une tâche échoue, il est important de savoir qu’elle a échoué et si une tâche réussit, il est important de savoir à quelle tâche la valeur de retour est associée. Par conséquent, vous devez accéder au résultat de la tâche retournée, ou l’attendre davantage, comme l’illustre cet exemple.
Comme avec WhenAll, vous devez être en mesure de prendre en charge les exceptions. Étant donné que vous recevez la tâche terminée, vous pouvez attendre la tâche retournée pour propager les erreurs, et utiliser try/catch
dessus comme nécessaire, par exemple :
Task<bool> [] recommendations = …;
while(recommendations.Count > 0)
{
Task<bool> recommendation = await Task.WhenAny(recommendations);
try
{
if (await recommendation) BuyStock(symbol);
break;
}
catch(WebException exc)
{
recommendations.Remove(recommendation);
}
}
En outre, même si une première tâche se termine correctement, les tâches suivantes peuvent échouer. À ce stade, vous avez plusieurs options pour traiter les exceptions : vous pouvez attendre que toutes les tâches lancées soient terminées, auquel cas vous pouvez utiliser la WhenAll méthode, ou vous pouvez décider que toutes les exceptions sont importantes et doivent être journalisées. Pour cela, vous pouvez utiliser des continuations pour recevoir une notification lorsque les tâches se sont terminées de manière asynchrone :
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => { if (t.IsFaulted) Log(t.Exception); });
}
ou:
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}
ou même :
private static async void LogCompletionIfFailed(IEnumerable<Task> tasks)
{
foreach(var task in tasks)
{
try { await task; }
catch(Exception exc) { Log(exc); }
}
}
…
LogCompletionIfFailed(recommendations);
Enfin, vous pouvez annuler toutes les opérations restantes :
var cts = new CancellationTokenSource();
var recommendations = new List<Task<bool>>()
{
GetBuyRecommendation1Async(symbol, cts.Token),
GetBuyRecommendation2Async(symbol, cts.Token),
GetBuyRecommendation3Async(symbol, cts.Token)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
cts.Cancel();
if (await recommendation) BuyStock(symbol);
Entrelacement
Considérez un cas où vous téléchargez des images à partir du web et traitez chaque image (par exemple, en ajoutant l’image à un contrôle d’interface utilisateur). Vous traitez les images de manière séquentielle sur le thread d’interface utilisateur, mais souhaitez télécharger les images aussi simultanément que possible. En outre, vous ne souhaitez pas retarder l'ajout des images à l'interface utilisateur jusqu'à ce qu'elles soient toutes téléchargées. Au lieu de cela, vous souhaitez les ajouter au fur et à mesure qu'ils sont complétés.
List<Task<Bitmap>> imageTasks =
(from imageUrl in urls select GetBitmapAsync(imageUrl)).ToList();
while(imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
panel.AddImage(image);
}
catch{}
}
Vous pouvez également appliquer l’entrelacement à un scénario qui implique un traitement gourmand en calcul sur les ThreadPool images téléchargées ; par exemple :
List<Task<Bitmap>> imageTasks =
(from imageUrl in urls select GetBitmapAsync(imageUrl)
.ContinueWith(t => ConvertImage(t.Result)).ToList();
while(imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
panel.AddImage(image);
}
catch{}
}
Limitation
Considérez l’exemple d’entrelacement, sauf que l’utilisateur télécharge tant d’images que les téléchargements doivent être limités ; par exemple, vous ne souhaitez qu’un nombre spécifique de téléchargements se produisent simultanément. Pour ce faire, vous pouvez démarrer un sous-ensemble des opérations asynchrones. À mesure que les opérations se terminent, vous pouvez démarrer des opérations supplémentaires pour qu’elles prennent place :
const int CONCURRENCY_LEVEL = 15;
Uri [] urls = …;
int nextIndex = 0;
var imageTasks = new List<Task<Bitmap>>();
while(nextIndex < CONCURRENCY_LEVEL && nextIndex < urls.Length)
{
imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
nextIndex++;
}
while(imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
panel.AddImage(image);
}
catch(Exception exc) { Log(exc); }
if (nextIndex < urls.Length)
{
imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
nextIndex++;
}
}
Interruption anticipée
Considérez que vous attendez de façon asynchrone qu’une opération se termine tout en répondant simultanément à la demande d’annulation d’un utilisateur (par exemple, l’utilisateur a cliqué sur un bouton Annuler). Le code suivant illustre ce scénario :
private CancellationTokenSource m_cts;
public void btnCancel_Click(object sender, EventArgs e)
{
if (m_cts != null) m_cts.Cancel();
}
public async void btnRun_Click(object sender, EventArgs e)
{
m_cts = new CancellationTokenSource();
btnRun.Enabled = false;
try
{
Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text);
await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
if (imageDownload.IsCompleted)
{
Bitmap image = await imageDownload;
panel.AddImage(image);
}
else imageDownload.ContinueWith(t => Log(t));
}
finally { btnRun.Enabled = true; }
}
private static async Task UntilCompletionOrCancellation(
Task asyncOp, CancellationToken ct)
{
var tcs = new TaskCompletionSource<bool>();
using(ct.Register(() => tcs.TrySetResult(true)))
await Task.WhenAny(asyncOp, tcs.Task);
return asyncOp;
}
Cette implémentation réactive l’interface utilisateur dès que vous décidez de procéder à un sauvetage, mais n’annule pas les opérations asynchrones sous-jacentes. Une autre alternative consisterait à annuler les opérations en attente lorsque vous décidez de procéder à un sauvetage, mais pas de rétablir l’interface utilisateur tant que les opérations ne sont pas terminées, potentiellement en raison de la fin anticipée en raison de la demande d’annulation :
private CancellationTokenSource m_cts;
public async void btnRun_Click(object sender, EventArgs e)
{
m_cts = new CancellationTokenSource();
btnRun.Enabled = false;
try
{
Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text, m_cts.Token);
await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
Bitmap image = await imageDownload;
panel.AddImage(image);
}
catch(OperationCanceledException) {}
finally { btnRun.Enabled = true; }
}
Un autre exemple de sauvetage anticipé implique l’utilisation de la WhenAny méthode conjointement avec la Delay méthode, comme indiqué dans la section suivante.
Task.Delay
Vous pouvez utiliser la Task.Delay méthode pour introduire des pauses dans l’exécution d’une méthode asynchrone. Cela est utile pour de nombreux types de fonctionnalités, notamment la création de boucles de sondage et le retardement du traitement des entrées utilisateur pendant une période prédéterminée. La méthode Task.Delay peut également être utile si elle est combinée avec Task.WhenAny pour implémenter des délais d’attente.
Si une tâche qui fait partie d’une opération asynchrone plus grande (par exemple, un service web ASP.NET) prend trop de temps, l’opération globale peut souffrir, en particulier si elle ne parvient pas à se terminer. Pour cette raison, il est important de pouvoir expirer en attendant une opération asynchrone. Les méthodes synchrones Task.Wait, Task.WaitAll, et Task.WaitAny acceptent les valeurs de délai d’attente, mais les méthodes correspondantes TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny et les méthodes Task.WhenAll/Task.WhenAny mentionnées précédemment ne les acceptent pas. Au lieu de cela, vous pouvez utiliser Task.Delay et Task.WhenAny en combinaison pour implémenter un délai d’attente.
Par exemple, dans votre application d’interface utilisateur, supposons que vous souhaitez télécharger une image et désactiver l’interface utilisateur pendant le téléchargement de l’image. Toutefois, si le téléchargement prend trop de temps, vous souhaitez réactiver l’interface utilisateur et ignorer le téléchargement :
public async void btnDownload_Click(object sender, EventArgs e)
{
btnDownload.Enabled = false;
try
{
Task<Bitmap> download = GetBitmapAsync(url);
if (download == await Task.WhenAny(download, Task.Delay(3000)))
{
Bitmap bmp = await download;
pictureBox.Image = bmp;
status.Text = "Downloaded";
}
else
{
pictureBox.Image = null;
status.Text = "Timed out";
var ignored = download.ContinueWith(
t => Trace("Task finally completed"));
}
}
finally { btnDownload.Enabled = true; }
}
La même chose s’applique à plusieurs téléchargements, car WhenAll retourne une tâche :
public async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.Enabled = false;
try
{
Task<Bitmap[]> downloads =
Task.WhenAll(from url in urls select GetBitmapAsync(url));
if (downloads == await Task.WhenAny(downloads, Task.Delay(3000)))
{
foreach(var bmp in downloads.Result) panel.AddImage(bmp);
status.Text = "Downloaded";
}
else
{
status.Text = "Timed out";
downloads.ContinueWith(t => Log(t));
}
}
finally { btnDownload.Enabled = true; }
}
Génération de combinateurs basés sur des tâches
Étant donné qu’une tâche est en mesure de représenter complètement une opération asynchrone et de fournir des fonctionnalités synchrones et asynchrones pour la jointure à l’opération, la récupération de ses résultats, et ainsi de suite, vous pouvez créer des bibliothèques utiles de combinateurs qui composent des tâches pour créer des modèles plus volumineux. Comme indiqué dans la section précédente, .NET inclut plusieurs combinateurs intégrés, mais vous pouvez également créer vos propres. Les sections suivantes fournissent plusieurs exemples de méthodes et de types de combinateurs potentiels.
RetryOnFault
Dans de nombreuses situations, vous souhaiterez peut-être réessayer une opération en cas d’échec d’une tentative précédente. Pour le code synchrone, vous pouvez créer une méthode d’assistance telle que RetryOnFault
dans l’exemple suivant pour effectuer cette opération :
public static T RetryOnFault<T>(
Func<T> function, int maxTries)
{
for(int i=0; i<maxTries; i++)
{
try { return function(); }
catch { if (i == maxTries-1) throw; }
}
return default(T);
}
Vous pouvez créer une méthode d’assistance presque identique pour les opérations asynchrones qui sont implémentées avec TAP et donc renvoyer des tâches :
public static async Task<T> RetryOnFault<T>(
Func<Task<T>> function, int maxTries)
{
for(int i=0; i<maxTries; i++)
{
try { return await function().ConfigureAwait(false); }
catch { if (i == maxTries-1) throw; }
}
return default(T);
}
Vous pouvez ensuite utiliser ce combinateur pour encoder les nouvelles tentatives dans la logique de l’application ; par exemple:
// Download the URL, trying up to three times in case of failure
string pageContents = await RetryOnFault(
() => DownloadStringTaskAsync(url), 3);
Vous pouvez étendre la RetryOnFault
fonction plus loin. Par exemple, la fonction peut accepter une autre Func<Task>
qui sera appelée entre les nouvelles tentatives pour déterminer quand réessayer l’opération ; par exemple :
public static async Task<T> RetryOnFault<T>(
Func<Task<T>> function, int maxTries, Func<Task> retryWhen)
{
for(int i=0; i<maxTries; i++)
{
try { return await function().ConfigureAwait(false); }
catch { if (i == maxTries-1) throw; }
await retryWhen().ConfigureAwait(false);
}
return default(T);
}
Vous pouvez ensuite utiliser la fonction comme suit pour attendre une seconde avant de réessayer l’opération :
// Download the URL, trying up to three times in case of failure,
// and delaying for a second between retries
string pageContents = await RetryOnFault(
() => DownloadStringTaskAsync(url), 3, () => Task.Delay(1000));
NeedOnlyOne
Parfois, vous pouvez tirer parti de la redondance pour améliorer la latence d’une opération et les chances de réussite. Considérez plusieurs services web qui fournissent des cotes d’actions, mais à différents moments de la journée, chaque service peut fournir différents niveaux de qualité et de temps de réponse. Pour gérer ces fluctuations, vous pouvez émettre des demandes à tous les services web, et dès que vous obtenez une réponse d’un, annulez les demandes restantes. Vous pouvez implémenter une fonction d’assistance pour faciliter l’implémentation de ce modèle commun de lancement de plusieurs opérations, en attendant tout, puis en annulant le reste. La NeedOnlyOne
fonction de l’exemple suivant illustre ce scénario :
public static async Task<T> NeedOnlyOne(
params Func<CancellationToken,Task<T>> [] functions)
{
var cts = new CancellationTokenSource();
var tasks = (from function in functions
select function(cts.Token)).ToArray();
var completed = await Task.WhenAny(tasks).ConfigureAwait(false);
cts.Cancel();
foreach(var task in tasks)
{
var ignored = task.ContinueWith(
t => Log(t), TaskContinuationOptions.OnlyOnFaulted);
}
return completed;
}
Vous pouvez ensuite utiliser cette fonction comme suit :
double currentPrice = await NeedOnlyOne(
ct => GetCurrentPriceFromServer1Async("msft", ct),
ct => GetCurrentPriceFromServer2Async("msft", ct),
ct => GetCurrentPriceFromServer3Async("msft", ct));
Opérations entrelacées
Il peut y avoir un problème potentiel de performance lorsqu'on utilise la méthode WhenAny pour gérer un scénario d'entrelacement avec de grands ensembles de tâches. Chaque appel à WhenAny entraîne une continuation enregistrée avec chaque tâche. Pour N nombre de tâches, cela entraîne des continuations O(N2) créées au cours de la durée de vie de l’opération d’entrelacement. Si vous utilisez un grand ensemble de tâches, vous pouvez utiliser un combinateur (Interleaved
dans l’exemple suivant) pour résoudre le problème de performances :
static IEnumerable<Task<T>> Interleaved<T>(IEnumerable<Task<T>> tasks)
{
var inputTasks = tasks.ToList();
var sources = (from _ in Enumerable.Range(0, inputTasks.Count)
select new TaskCompletionSource<T>()).ToList();
int nextTaskIndex = -1;
foreach (var inputTask in inputTasks)
{
inputTask.ContinueWith(completed =>
{
var source = sources[Interlocked.Increment(ref nextTaskIndex)];
if (completed.IsFaulted)
source.TrySetException(completed.Exception.InnerExceptions);
else if (completed.IsCanceled)
source.TrySetCanceled();
else
source.TrySetResult(completed.Result);
}, CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
return from source in sources
select source.Task;
}
Vous pouvez ensuite utiliser le combinateur pour traiter les résultats des tâches à mesure qu’elles se terminent ; par exemple:
IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
int result = await task;
…
}
WhenAllOrFirstException
Dans certains scénarios de dispersion/regroupement, vous souhaiterez attendre toutes les tâches dans un jeu, à moins qu’une d’elles rencontre une erreur, auquel cas vous souhaiterez cesser d’attendre dès que l’exception se produit. Pour ce faire, vous pouvez utiliser une méthode de combinateur comme WhenAllOrFirstException
dans l’exemple suivant :
public static Task<T[]> WhenAllOrFirstException<T>(IEnumerable<Task<T>> tasks)
{
var inputs = tasks.ToList();
var ce = new CountdownEvent(inputs.Count);
var tcs = new TaskCompletionSource<T[]>();
Action<Task> onCompleted = (Task completed) =>
{
if (completed.IsFaulted)
tcs.TrySetException(completed.Exception.InnerExceptions);
if (ce.Signal() && !tcs.Task.IsCompleted)
tcs.TrySetResult(inputs.Select(t => t.Result).ToArray());
};
foreach (var t in inputs) t.ContinueWith(onCompleted);
return tcs.Task;
}
Création de structures de données basées sur des tâches
En plus de permettre la création de combinateurs personnalisés basés sur des tâches, avoir une structure de données dans Task et Task<TResult> qui représente à la fois les résultats d’une opération asynchrone et la synchronisation nécessaire pour s’y joindre en fait un type puissant, sur lequel il est possible de construire des structures de données personnalisées pour être utilisées dans des scénarios asynchrones.
AsyncCache
Un aspect important d'une tâche est qu'elle peut être transmise à plusieurs consommateurs, qui peuvent tous l'attendre, enregistrer des continuations avec elle, obtenir son résultat ou ses exceptions (dans le cas de Task<TResult>), et ainsi de suite. Cela rend Task et Task<TResult> convient parfaitement à l’utilisation dans une infrastructure de mise en cache asynchrone. Voici un exemple de cache asynchrone petit mais puissant basé sur Task<TResult>:
public class AsyncCache<TKey, TValue>
{
private readonly Func<TKey, Task<TValue>> _valueFactory;
private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map;
public AsyncCache(Func<TKey, Task<TValue>> valueFactory)
{
if (valueFactory == null) throw new ArgumentNullException("valueFactory");
_valueFactory = valueFactory;
_map = new ConcurrentDictionary<TKey, Lazy<Task<TValue>>>();
}
public Task<TValue> this[TKey key]
{
get
{
if (key == null) throw new ArgumentNullException("key");
return _map.GetOrAdd(key, toAdd =>
new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
}
}
}
La classe AsyncCache<TKey,TValue> accepte en tant que délégué à son constructeur une fonction qui prend un TKey
et retourne un Task<TResult>. Toutes les valeurs précédemment consultées à partir du cache sont stockées dans le dictionnaire interne et AsyncCache
garantissent qu’une seule tâche est générée par clé, même si le cache est accessible simultanément.
Par exemple, vous pouvez créer un cache pour les pages web téléchargées :
private AsyncCache<string,string> m_webPages =
new AsyncCache<string,string>(DownloadStringTaskAsync);
Vous pouvez ensuite utiliser ce cache dans des méthodes asynchrones chaque fois que vous avez besoin du contenu d’une page web. La AsyncCache
classe garantit que vous téléchargez le moins de pages possible et met en cache les résultats.
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
btnDownload.IsEnabled = false;
try
{
txtContents.Text = await m_webPages["https://www.microsoft.com"];
}
finally { btnDownload.IsEnabled = true; }
}
AsyncProducerConsumerCollection
Vous pouvez également utiliser des tâches pour créer des structures de données pour coordonner les activités asynchrones. Considérez l’un des modèles de conception parallèle classiques : producteur/consommateur. Dans ce modèle, les producteurs génèrent des données consommées par les consommateurs, et les producteurs et les consommateurs peuvent s’exécuter en parallèle. Par exemple, le consommateur traite l’élément 1, qui a été généré précédemment par un producteur qui produit maintenant l’élément 2. Pour le modèle producteur/consommateur, vous avez toujours besoin d’une structure de données pour stocker le travail créé par les producteurs afin que les consommateurs puissent être avertis des nouvelles données et les trouver lorsqu’elles sont disponibles.
Voici une structure de données simple, basée sur des tâches, qui permet aux méthodes asynchrones d’être utilisées en tant que producteurs et consommateurs :
public class AsyncProducerConsumerCollection<T>
{
private readonly Queue<T> m_collection = new Queue<T>();
private readonly Queue<TaskCompletionSource<T>> m_waiting =
new Queue<TaskCompletionSource<T>>();
public void Add(T item)
{
TaskCompletionSource<T> tcs = null;
lock (m_collection)
{
if (m_waiting.Count > 0) tcs = m_waiting.Dequeue();
else m_collection.Enqueue(item);
}
if (tcs != null) tcs.TrySetResult(item);
}
public Task<T> Take()
{
lock (m_collection)
{
if (m_collection.Count > 0)
{
return Task.FromResult(m_collection.Dequeue());
}
else
{
var tcs = new TaskCompletionSource<T>();
m_waiting.Enqueue(tcs);
return tcs.Task;
}
}
}
}
Avec cette structure de données en place, vous pouvez écrire du code comme suit :
private static AsyncProducerConsumerCollection<int> m_data = …;
…
private static async Task ConsumerAsync()
{
while(true)
{
int nextItem = await m_data.Take();
ProcessNextItem(nextItem);
}
}
…
private static void Produce(int data)
{
m_data.Add(data);
}
L’espace System.Threading.Tasks.Dataflow de noms inclut le BufferBlock<T> type, que vous pouvez utiliser de la même manière, mais sans avoir à créer un type de collection personnalisé :
private static BufferBlock<int> m_data = …;
…
private static async Task ConsumerAsync()
{
while(true)
{
int nextItem = await m_data.ReceiveAsync();
ProcessNextItem(nextItem);
}
}
…
private static void Produce(int data)
{
m_data.Post(data);
}
Remarque
L'espace de noms System.Threading.Tasks.Dataflow est disponible en tant que package NuGet. Pour installer l’assembly qui contient l’espace System.Threading.Tasks.Dataflow de noms, ouvrez votre projet dans Visual Studio, choisissez Gérer les packages NuGet dans le menu Projet et recherchez en ligne le System.Threading.Tasks.Dataflow
package.