web-dev-qa-db-fra.com

Async attendre dans linq, sélectionnez

Je dois 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 async et await dans la sélection. Selon cette réponse de Stephen Cleary, je devrais pouvoir les laisser tomber. 

Puis la seconde 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, le cas échéant, le reste de la requête sera 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 très intéressé par le changement de code asynchrone (apparemment fonctionnel). Peut-être que je suis juste inquiet pour rien et que les 3 exemples de code font exactement la même chose?

ProcessEventsAsync ressemble à ceci:

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}
97
Alexander Derck
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 l’attente dans la sélection. Selon cette réponse de Stephen Cleary, je devrais pouvoir les laisser tomber.

L'appel à Select est 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 quant à la manière dont une exception synchrone serait levée de ProcessEventAsync, mais dans le contexte de ce code, cela n'a aucune importance.)

Puis la deuxième sélection 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, le cas échéant, le reste de la requête sera exécuté?

Cela signifie que la requête est bloquante. Donc, ce n'est pas vraiment asynchrone.

Le décomposer:

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

commencera 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 par une (il attend d'abord l'opération du premier événement, puis le suivant, puis le suivant, etc.).

C'est la partie qui m'importe peu, car elle bloque et engloberait toutes les exceptions dans 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 lancent tous les deux toutes les opérations asynchrones (events.Select(...)), puis attendent de manière asynchrone la fin de toutes les opérations dans un ordre quelconque (await Task.WhenAll(...)), puis poursuivent le travail restant (Where...).

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

105
Stephen Cleary

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 en attente de la fin de chaque nouvelle tâche.

D'autre part, 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, la variable doit donc ê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

16
tede24

Avec les méthodes actuelles disponibles à 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.

6
Vitaliy Ulantikov

Je préfère ceci 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 une méthode de chaînage:

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

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));
0
Siderite Zackwehdex