Come attendere un elenco di attività in modo asincrono utilizzando LINQ?


88

Ho un elenco di attività che ho creato in questo modo:

public async Task<IList<Foo>> GetFoosAndDoSomethingAsync()
{
    var foos = await GetFoosAsync();

    var tasks = foos.Select(async foo => await DoSomethingAsync(foo)).ToList();

    ...
}

Utilizzando .ToList(), le attività dovrebbero iniziare tutte. Ora voglio attendere il loro completamento e restituire i risultati.

Funziona nel ...blocco sopra :

var list = new List<Foo>();
foreach (var task in tasks)
    list.Add(await task);
return list;

Fa quello che voglio, ma sembra piuttosto goffo. Preferisco di gran lunga scrivere qualcosa di più semplice come questo:

return tasks.Select(async task => await task).ToList();

... ma questo non viene compilato. Cosa mi sto perdendo? O semplicemente non è possibile esprimere le cose in questo modo?


Devi elaborare in DoSomethingAsync(foo)serie per ogni foo, o questo è un candidato per Parallel.ForEach <Foo> ?
mdisibio

1
@mdisibio - Parallel.ForEachsta bloccando. Il modello qui proviene dal video Asynchronous C # di Jon Skeet su Pluralsight . Viene eseguito in parallelo senza bloccare.
Matt Johnson-Pint

@mdisibio - No. Corrono in parallelo. Provalo . (Inoltre, sembra che non ne abbia bisogno .ToList()se lo userò WhenAll.)
Matt Johnson-Pint

Punto preso. A seconda di come DoSomethingAsyncè scritto, l'elenco potrebbe o meno essere eseguito in parallelo. Sono stato in grado di scrivere un metodo di test che era e una versione che non lo era, ma in entrambi i casi il comportamento è dettato dal metodo stesso, non dal delegato che crea l'attività. Ci scusiamo per la confusione. Tuttavia, se DoSomethingAsycritorna Task<Foo>, allora il awaitdelegato non è assolutamente necessario ... Penso che fosse il punto principale che stavo per provare a fare.
mdisibio

Risposte:


138

LINQ non funziona perfettamente con il asynccodice, ma puoi farlo:

var tasks = foos.Select(DoSomethingAsync).ToList();
await Task.WhenAll(tasks);

Se le tue attività restituiscono tutte lo stesso tipo di valore, puoi anche farlo:

var results = await Task.WhenAll(tasks);

che è abbastanza carino. WhenAllrestituisce un array, quindi credo che il tuo metodo possa restituire direttamente i risultati:

return await Task.WhenAll(tasks);

11
Volevo solo sottolineare che questo può funzionare anche convar tasks = foos.Select(foo => DoSomethingAsync(foo)).ToList();
mdisibio

1
o anchevar tasks = foos.Select(DoSomethingAsync).ToList();
Todd Menier

3
qual è il motivo per cui Linq non funziona perfettamente con il codice asincrono?
Ehsan Sajjad,

2
@ EhsanSajjad: perché LINQ to Objects funziona in modo sincrono sugli oggetti in memoria. Alcune cose limitate funzionano, come Select. Ma la maggior parte non piace Where.
Stephen Cleary,

4
@ EhsanSajjad: se l'operazione è basata su I / O, puoi utilizzare asyncper ridurre i thread; se è vincolato alla CPU e già su un thread in background, asyncnon fornirebbe alcun vantaggio.
Stephen Cleary,

9

Per espandere la risposta di Stephen, ho creato il seguente metodo di estensione per mantenere lo stile fluente di LINQ. Puoi quindi fare

await someTasks.WhenAll()

namespace System.Linq
{
    public static class IEnumerableExtensions
    {
        public static Task<T[]> WhenAll<T>(this IEnumerable<Task<T>> source)
        {
            return Task.WhenAll(source);
        }
    }
}

10
Personalmente, ToArrayAsync
chiamerei il

4

Un problema con Task.WhenAll è che creerebbe un parallelismo. Nella maggior parte dei casi potrebbe essere anche meglio, ma a volte vuoi evitarlo. Ad esempio, la lettura di dati in batch dal DB e l'invio di dati a un servizio Web remoto. Non si desidera caricare tutti i batch in memoria, ma premere il DB una volta che il batch precedente è stato elaborato. Quindi, devi rompere l'asincronicità. Ecco un esempio:

var events = Enumerable.Range(0, totalCount/ batchSize)
   .Select(x => x*batchSize)
   .Select(x => dbRepository.GetEventsBatch(x, batchSize).GetAwaiter().GetResult())
   .SelectMany(x => x);
foreach (var carEvent in events)
{
}

Nota .GetAwaiter (). GetResult () che lo converte in sincronizzazione. Il DB verrebbe colpito in modo pigro solo una volta che batchSize degli eventi è stato elaborato.


1

Utilizzare Task.WaitAllo Task.WhenAllqualunque sia appropriato.


1
Neanche questo funziona. Task.WaitAllsta bloccando, non è in attesa e non funziona con un file Task<T>.
Matt Johnson-Pint

@MattJohnson WhenAll?
LB

Sì. Questo è tutto! Mi sento stupido. Grazie!
Matt Johnson-Pint

0

Task.WhenAll dovrebbe fare il trucco qui.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.