Disons que j'ai l'exemple de code suivant:
0 before 1 before 2 before 2 after 0 after 1 after
La TestMethod fonctionnera jusqu'à la fin 3 fois, donc je verrai 3 paires de avant
et after
:
private static async Task<int> TestMethod(int param) { Console.WriteLine($"{param} before"); await Task.Delay(50); Console.WriteLine($"{param} after"); return param; }
Maintenant, je dois rendre TestMethod asynchrone:
0 before 0 after 1 before 1 after 2 before 2 after
Comment puis-je J'écris une expression Select similaire pour cette méthode asynchrone? Si j'utilise juste un lambda asynchrone Enumerable.Range (0, 3) .Select (async x => wait TestMethod (x)). ToArray ();
, cela ne fonctionnera pas car il a gagné ' t attendre la fin, donc avant
les parties seront appelées en premier:
private static async Task Main(string[] args) { var result = Enumerable.Range(0, 3).Select(x => TestMethod(x)).ToArray(); Console.ReadKey(); } private static int TestMethod(int param) { Console.WriteLine($"{param} before"); Thread.Sleep(50); Console.WriteLine($"{param} after"); return param; }
Notez que je ne veux pas exécuter les 3 appels en parallèle - J'ai besoin qu'ils s'exécutent un par un, en commençant le suivant uniquement lorsque le précédent est complètement terminé et a renvoyé la valeur.
6 Réponses :
Vous pouvez utiliser les primitives de synchronisation
class Program { static async Task Main(string[] args) { var waitHandle = new AutoResetEvent(true); var result = Enumerable .Range(0, 3) .Select(async (int param) => { waitHandle.WaitOne(); await TestMethod(param); waitHandle.Set(); }).ToArray(); Console.ReadKey(); } private static async Task<int> TestMethod(int param) { Console.WriteLine($"{param} before"); await Task.Delay(50); Console.WriteLine($"{param} after"); return param; } }
Je rencontre régulièrement cette exigence et je ne connais aucune solution intégrée pour y répondre comme en C # 7.2. Je reviens généralement à l'utilisation de await
sur chaque opération asynchrone dans un foreach
, mais vous pouvez choisir votre méthode d'extension:
static async Task Main(string[] args) { var result = (await Enumerable.Range(0, 3).SelectAsync(x => TestMethod(x))).ToArray(); Console.ReadKey(); }
Vous appelleriez alors wait
sur le SelectAsync
:
public static class EnumerableExtensions { public static async Task<IEnumerable<TResult>> SelectAsync<TSource, TResult>( this IEnumerable<TSource> source, Func<TSource, Task<TResult>> asyncSelector) { var results = new List<TResult>(); foreach (var item in source) results.Add(await asyncSelector(item)); return results; } }
L'inconvénient de cette approche est que le SelectAsync
est impatient, pas paresseux. C # 8 promet d'introduire des flux asynchrones , ce qui permettra de redevenir paresseux.
Belle solution! Le type de retour Task
pourrait cependant créer de fausses attentes d'évaluation paresseuse. Le renvoi de Task
pourrait être plus approprié.
Vous devez être conscient de ce qu'est un objet Enumerable. Un objet Enumerable n'est pas l'énumération elle-même. Cela vous donne la possibilité d'obtenir un objet Enumerator. Si vous avez un objet Enumerator, vous pouvez demander les premiers éléments d'une séquence, et une fois que vous avez un élément énuméré, vous pouvez demander le suivant.
Donc, créer un objet qui vous permet d'énumérer sur une séquence, n'a rien à voir avec l'énumération elle-même.
Votre fonction Select renvoie uniquement un objet Enumerable, elle ne démarre pas l'énumération. L'énumération est lancée par ToArray
.
Au niveau le plus bas, l'énumération se fait comme suit:
var result = Enumerable.Range(0, 3) .Select(i => TestMethod(i)) .ToListAsync();
ToArray
appellera en interne GetEnumerator
et MoveNext
. Donc, pour rendre votre instruction LINQ asynchrone, vous aurez besoin d'un ToArrayAsync
.
Le code est assez simple. Je vais vous montrer ToListAsync comme une méthode d'extension, ce qui est un peu plus facile à faire.
static class EnumerableAsyncExtensions { public static async Task<List<TSource>> ToListAsync<TSource>( this IEnumerable<Task<TSource>> source) { List<TSource> result = new List<TSource>(); var enumerator = source.GetEnumerator() while (enumerator.MoveNext()) { // in baby steps. Feel free to do this in one step Task<TSource> current = enumerator.Current; TSource awaitedCurrent = await current; result.Add(awaitedCurrent); } return result; } }
Vous n'aurez à créer cela qu'une seule fois. Vous pouvez l'utiliser pour n'importe quel ToListAsync où vous devrez attendre pour chaque élément:
IEnumerable<TSource> myEnumerable = ... IEnumerator<TSource> enumerator = myEnumerable.GetEnumerator(); while (enumerator.MoveNext()) { // there is still an element in the enumeration TSource currentElement = enumerator.Current; Process(currentElement); }
Notez que le retour de Select
est IEnumerable
: un objet qui permet d'énumérer une séquence d'objets Task
. Dans un foreach, à chaque cycle, vous obtenez une Task
Je me demande ce que j'ai fait de mal pour mériter un vote défavorable. Si vous ne commentez pas lorsque vous votez contre, je ne peux pas améliorer ma réponse et je ne saurai jamais ce qui ne va pas
Je me demande aussi pourquoi quelqu'un a voté contre. Informations très intéressantes sur Enumerable
, je ne m'en rendais pas compte. Je suis arrivé à une méthode d'extension ToListAsync
très similaire, sauf que j'ai utilisé foreach
au lieu de travailler directement avec Enumerable (voir la réponse que j'ai ajoutée). Je crois comprendre que foreach
fait fondamentalement la même chose sous le capot. Est-ce correct ou est-ce que je rate quelque chose?
foreach appelle en interne GetEnumerator ()
/ MoveNext ()
/ Current
. Il est donc généralement beaucoup plus facile d'utiliser foreach et yield return
si vous souhaitez renvoyer un IEnumerable<...>
au lieu d'une List <...>
. Je n'utilise GetEnumerator
et MoveNext
que si l'énumération dépend de ce que j'ai lu. Un bon exemple est Any
. Essayez de coder cela en utilisant un foreach et comparez-le avec le simple return enumerator.MoveNext ();
Cette solution est un peu dangereuse, car elle dépend du fait que la source IEnumerable
n'est pas matérialisée. L'appelant pourrait facilement faire l'erreur de matérialiser l'énumérable différé des tâches, en appelant ToList
par exemple, et cela changerait complètement le comportement en démarrant toutes les tâches à la fois. Pour cette raison, je suggérerais d'ajouter une vérification que la source
ne peut pas être convertie en collection matérialisée: if (source is ICollection
Dans C # 8 ( la prochaine version majeure au moment de la rédaction) il y aura un support pour IAsyncEnumrable
où vous pouvez écrire async pour chaque boucle:
await foreach (var item in GetItemsAsync()) // Do something to each item in sequence
J'attends là sera une méthode d'extension Select
pour faire une projection, mais sinon, il n'est pas difficile d'écrire la vôtre. Vous pourrez également créer des blocs d'itérateur IAsyncEnumrable
avec yield return
.
On dirait qu'il n'y a pas d'autre solution actuellement (jusqu'à C # 8.0) à part l'énumérer manuellement. J'ai créé une méthode d'extension pour cela (renvoie une liste au lieu d'un tableau car c'est plus simple):
public static async Task<List<T>> ToListAsync<T>(this IEnumerable<Task<T>> source) { var result = new List<T>(); foreach (var item in source) { result.Add(await item); } return result; }
Je suppose que c'est ce que OP veut réellement, puisque j'ai passé une journée entière à résoudre la même question que se demandait OP. Mais je veux que cela se fasse en mehtod couramment.
Par la réponse de Martin Liversage, j'ai trouvé le bon moyen: IAsyncEnumerable
et System.Linq.Async
pacakge, qui sont disponible en C # 8.0, c'est ce que je veux.
Exemple:
private static async Task Main(string[] args) { var result = await Enumerable.Range(0, 3) .ToAsyncEnumerable() .SelectAwait(x => TestMethod(x)) .ToArrayAsync(); Console.ReadKey(); } private static async Task<int> TestMethod(int param) { Console.WriteLine($"{param} before"); await Task.Delay(50); Console.WriteLine($"{param} after"); return param; }
Agréable. Mais avez-vous oublié une attente? var result = wait Enumerable ...
Oui. Désolé pour ça.
Belle solution! Hélas, les énumérables asynchrones ne sont pris en charge que pour .NET Core, pas pour .NET Framework.
@AlekseyShubin, vous pouvez également utiliser des IAsyncEnumerable
sur .NET Framework. Vous devez d'abord installer le package Microsoft.Bcl.AsyncInterfaces, puis ajouter la ligne
Et si vous utilisiez simplement
foreach
?@JSteward est certainement une option, mais le style LINQ est juste beaucoup plus court et élégant. Je vais créer une méthode d'extension enveloppant
foreach
, j'espérais juste qu'il existe déjà une solution dans la bibliothèque standard.@Aleksey Shubin Considérez-vous ma réponse comme correcte ou non?
Vous dites donc que d'autres exigences stipulent que
TestMethod
doit êtreasync
, mais dans ce cas particulier, vous voulez le maintenir en fonctionnement synchrone? Sinon, je ne vois aucune raison de rendre la méthodeasync
. Si oui, vous pouvez renvoyerTestMethod (x) .Result
.@ArturMustafin non, dans votre réponse, vous bloquez le thread, perdant tous les avantages de l'exécution asynchrone. L'appel de
TestMethod (x) .Result
serait le même et plus facile.@GertArnold Je veux l'exécuter de manière asynchrone mais pas en parallèle - des choses un peu différentes. Dans ma vraie tâche, je voulais faire plusieurs appels à
DbContext.SaveChangesAsync
Entity Framework qui ne permet pas une exécution parallèle, mais il y a toujours un avantage de l'appel asynchrone - il ne bloque pas le fil d'appel.@AlekseyShubin "Notez que je ne veux pas exécuter les 3 appels en parallèle - j'ai besoin qu'ils s'exécutent un par un, en commençant le suivant uniquement lorsque le précédent est complètement terminé et a renvoyé la valeur."
@ArturMustafin Je ne vois pas votre point. Oui, je ne le souhaite pas en parallèle, mais je le souhaite toujours asynchrone - voir ce commentaire pour plus de détails. Votre réponse le rend synchrone.