Async attendent dans linq select

181

J'ai besoin de modifier un programme existant et il contient le code suivant:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

Mais cela me semble très étrange, tout d'abord l'utilisation de asyncet awaitdans le select. D'après cette réponse de Stephen Cleary, je devrais pouvoir les supprimer.

Puis le second Selectqui sélectionne le résultat. Cela ne signifie-t-il pas que la tâche n'est pas du tout asynchrone et est exécutée de manière synchrone (tant d'efforts pour rien), ou la tâche sera-t-elle exécutée de manière asynchrone et lorsqu'elle est terminée, le reste de la requête est exécuté?

Dois-je écrire le code ci-dessus comme suit selon une autre réponse de Stephen Cleary :

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

et est-ce complètement pareil?

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

Pendant que je travaille sur ce projet, j'aimerais changer le premier exemple de code, mais je ne suis pas trop désireux de changer (apparemment en train de travailler) le code async. Peut-être que je ne m'inquiète pour rien et que les 3 exemples de code font exactement la même chose?

ProcessEventsAsync ressemble à ceci:

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}
Alexandre Derck
la source
Quel est le type de retour de ProceesEventAsync?
tede24 du
@ tede24 Il est Task<InputResult>avec InputResultêtre une classe personnalisée.
Alexander Derck
Vos versions sont beaucoup plus faciles à lire à mon avis. Cependant, vous avez oublié Selectles résultats des tâches avant votre Where.
Max
Et InputResult a un droit de propriété Result?
tede24 du
@ tede24 Le résultat est la propriété de la tâche et non ma classe. Et @Max l'attente devrait s'assurer que j'obtiens les résultats sans accéder à la Resultpropriété de la tâche
Alexander Derck

Réponses:

185
var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

Mais cela me semble très étrange, tout d'abord l'utilisation de l'async et attendre dans le select. D'après cette réponse de Stephen Cleary, je devrais pouvoir les supprimer.

L'appel à Selectest valide. Ces deux lignes sont essentiellement identiques:

events.Select(async ev => await ProcessEventAsync(ev))
events.Select(ev => ProcessEventAsync(ev))

(Il y a une différence mineure concernant la façon dont une exception synchrone serait lancée ProcessEventAsync, mais dans le contexte de ce code, cela n'a pas d'importance.)

Puis le second Select qui sélectionne le résultat. Cela ne signifie-t-il pas que la tâche n'est pas du tout asynchrone et qu'elle est exécutée de manière synchrone (tant d'efforts pour rien), ou la tâche sera-t-elle exécutée de manière asynchrone et lorsqu'elle est terminée, le reste de la requête est exécuté?

Cela signifie que la requête est bloquante. Ce n'est donc pas vraiment asynchrone.

Décomposer:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))

démarrera d'abord une opération asynchrone pour chaque événement. Puis cette ligne:

                   .Select(t => t.Result)

attendra que ces opérations se terminent une à la fois (il attend d'abord l'opération du premier événement, puis la suivante, puis la suivante, etc.).

C'est la partie qui ne m'intéresse pas, car elle bloque et englobe également toutes les exceptions AggregateException.

et est-ce complètement pareil?

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

Oui, ces deux exemples sont équivalents. Ils démarrent tous les deux toutes les opérations asynchrones ( events.Select(...)), puis attendent de manière asynchrone que toutes les opérations se terminent dans n'importe quel ordre ( await Task.WhenAll(...)), puis poursuivent le reste du travail ( Where...).

Ces deux exemples sont différents du code d'origine. Le code d'origine est bloquant et encapsulera les exceptions AggregateException.

Stephen Cleary
la source
Vive pour éclaircir cela! Donc, au lieu des exceptions enveloppées dans un AggregateException, j'obtiendrais plusieurs exceptions distinctes dans le deuxième code?
Alexander Derck
1
@AlexanderDerck: Non, dans l'ancien et le nouveau code, seule la première exception serait levée. Mais avec Resultça serait enveloppé AggregateException.
Stephen Cleary
J'obtiens un blocage dans mon contrôleur ASP.NET MVC en utilisant ce code. Je l'ai résolu en utilisant Task.Run (…). Je n'ai pas un bon pressentiment à ce sujet. Cependant, il s'est terminé correctement lors de l'exécution d'un test xUnit asynchrone. Que se passe-t-il?
SuperJMN
2
@SuperJMN: Remplacer stuff.Select(x => x.Result);parawait Task.WhenAll(stuff)
Stephen Cleary
1
@DanielS: Ils sont essentiellement les mêmes. Il existe quelques différences telles que les machines à états, la capture de contexte, le comportement des exceptions synchrones. Plus d'infos sur blog.stephencleary.com/2016/12/eliding-async-await.html
Stephen Cleary
25

Le code existant fonctionne, mais bloque le thread.

.Select(async ev => await ProcessEventAsync(ev))

crée une nouvelle tâche pour chaque événement, mais

.Select(t => t.Result)

bloque le thread qui attend la fin de chaque nouvelle tâche.

En revanche, votre code produit le même résultat mais reste asynchrone.

Juste un commentaire sur votre premier code. Cette ligne

var tasks = await Task.WhenAll(events...

produira une seule tâche donc la variable doit être nommée au singulier.

Enfin votre dernier code fait la même chose mais est plus succinct

Pour référence: Task.Wait / Task.WhenAll

tede24
la source
Donc, le premier bloc de code est en fait exécuté de manière synchrone?
Alexander Derck le
1
Oui, car l'accès à Result produit un Wait qui bloque le thread. D'autre part, lorsque produit une nouvelle tâche que vous pouvez attendre.
tede24 du
1
Pour en revenir à cette question et en regardant votre remarque sur le nom de la tasksvariable, vous avez tout à fait raison. Choix horrible, ce ne sont même pas des tâches car elles sont attendues tout de suite. Je vais laisser la question telle
quelle
13

Avec les méthodes actuelles disponibles dans Linq, cela semble assez moche:

var tasks = items.Select(
    async item => new
    {
        Item = item,
        IsValid = await IsValid(item)
    });
var tuples = await Task.WhenAll(tasks);
var validItems = tuples
    .Where(p => p.IsValid)
    .Select(p => p.Item)
    .ToList();

Espérons que les versions suivantes de .NET proposeront des outils plus élégants pour gérer des collections de tâches et des tâches de collections.

Vitaliy Ulantikov
la source
13

J'ai utilisé ce code:

public static async Task<IEnumerable<TResult>> SelectAsync<TSource,TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> method)
{
      return await Task.WhenAll(source.Select(async s => await method(s)));
}

comme ça:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params));
Sidérite Zackwehdex
la source
5
Cela enveloppe juste la fonctionnalité existante d'une manière plus obscure imo
Alexander Derck
L'alternative est var result = await Task.WhenAll (sourceEnumerable.Select (async s => wait someFunction (s, other params)). Cela fonctionne aussi, mais ce n'est pas LINQy
Siderite Zackwehdex
2
Les paramètres supplémentaires sont externes, selon la fonction que je souhaite exécuter, ils ne sont pas pertinents dans le contexte de la méthode d'extension.
Siderite Zackwehdex
5
C'est une belle méthode d'extension. Je ne sais pas pourquoi il a été jugé "plus obscur" - il est sémantiquement analogue au synchrone Select(), il en va de même pour un élégant drop-in.
nullPainter
1
Le asyncet awaità l'intérieur du premier lambda est redondant. La méthode SelectAsync peut simplement être écrite comme suit:return await Task.WhenAll(source.Select(method));
Nathan
12

Je préfère cela comme méthode d'extension:

public static async Task<IEnumerable<T>> WhenAll<T>(this IEnumerable<Task<T>> tasks)
{
    return await Task.WhenAll(tasks);
}

Pour qu'il soit utilisable avec le chaînage de méthodes:

var inputs = await events
  .Select(async ev => await ProcessEventAsync(ev))
  .WhenAll()
Daryl
la source
1
Vous ne devriez pas appeler la méthode Waitlorsqu'elle n'est pas en attente. Il s'agit de créer une tâche qui est terminée lorsque toutes les tâches sont terminées. Appelez-le WhenAll, comme la Taskméthode qu'il émule. Il est également inutile que la méthode soit async. Il suffit d'appeler WhenAllet d'en finir.
Servy
Un peu un wrapper inutile à mon avis quand il appelle simplement la méthode originale
Alexander Derck
@Servy juste point, mais je n'aime pas particulièrement les options de nom. WhenAll fait que cela ressemble à un événement qui n'est pas tout à fait.
Daryl
3
@AlexanderDerck l'avantage est que vous pouvez l'utiliser dans le chaînage de méthodes.
Daryl
2
@Daryl car WhenAllrenvoie une liste évaluée (elle n'est pas évaluée paresseusement), un argument peut être fait pour utiliser le Task<T[]>type de retour pour signifier cela. Lorsqu'il est attendu, cela pourra toujours utiliser Linq, mais communique également qu'il n'est pas paresseux.
JAD