Bagaimana cara menunggu daftar tugas secara asynchronous menggunakan LINQ?

88

Saya memiliki daftar tugas yang saya buat seperti ini:

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

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

    ...
}

Dengan menggunakan .ToList(), semua tugas harus dimulai. Sekarang saya ingin menunggu penyelesaian mereka dan mengembalikan hasilnya.

Ini berfungsi di ...blok di atas :

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

Itu melakukan apa yang saya inginkan, tetapi ini tampaknya agak kikuk. Saya lebih suka menulis sesuatu yang lebih sederhana seperti ini:

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

... tapi ini tidak bisa dikompilasi. Apa yang saya lewatkan? Atau apakah tidak mungkin untuk mengungkapkan hal-hal seperti ini?

Matt Johnson-Pint
sumber
Apakah Anda perlu memproses DoSomethingAsync(foo)secara serial untuk setiap foo, atau apakah ini kandidat untuk Parallel.ForEach <Foo> ?
mdisibio
1
@mdisibio - Parallel.ForEachmemblokir. Polanya di sini berasal dari video Asynchronous C # Jon Skeet di Pluralsight . Ini dijalankan secara paralel tanpa pemblokiran.
Matt Johnson-Pint
@mdisibio - Tidak. Mereka berjalan secara paralel. Cobalah . (Selain itu, sepertinya saya tidak perlu .ToList()jika saya hanya akan menggunakan WhenAll.)
Matt Johnson-Pint
Poin diambil. Bergantung pada cara DoSomethingAsyncpenulisannya, daftar mungkin atau mungkin tidak dijalankan secara paralel. Saya dapat menulis metode pengujian yang dulu dan versi yang tidak, tetapi dalam kedua kasus perilakunya ditentukan oleh metode itu sendiri, bukan delegasi yang membuat tugas. Maaf atas kesalahannya. Namun, jika DoSomethingAsyckembali Task<Foo>, maka awaitdelegasi tidak mutlak diperlukan ... Saya pikir itu adalah poin utama yang akan saya coba buat.
mdisibio

Jawaban:

138

LINQ tidak berfungsi sempurna dengan asynckode, tetapi Anda dapat melakukan ini:

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

Jika semua tugas Anda mengembalikan jenis nilai yang sama, Anda bahkan dapat melakukan ini:

var results = await Task.WhenAll(tasks);

yang mana cukup bagus. WhenAllmengembalikan array, jadi saya yakin metode Anda dapat mengembalikan hasilnya secara langsung:

return await Task.WhenAll(tasks);
Stephen Cleary
sumber
11
Hanya ingin menunjukkan bahwa ini juga dapat berfungsi denganvar tasks = foos.Select(foo => DoSomethingAsync(foo)).ToList();
mdisibio
1
atau bahkanvar tasks = foos.Select(DoSomethingAsync).ToList();
Todd Menier
3
apa alasan di balik itu bahwa Linq tidak bekerja dengan sempurna dengan kode async?
Ehsan Sajjad
2
@EhsanSajjad: Karena LINQ ke Objek bekerja secara sinkron pada objek dalam memori. Beberapa hal terbatas berfungsi, seperti Select. Tapi kebanyakan tidak, suka Where.
Stephen Cleary
4
@EhsanSajjad: Jika operasinya berbasis I / O, maka Anda dapat menggunakan asyncuntuk mengurangi thread; jika terikat dengan CPU dan sudah ada di thread latar belakang, maka asynctidak akan memberikan manfaat apa pun.
Stephen Cleary
9

Untuk memperluas jawaban Stephen, saya telah membuat metode ekstensi berikut untuk menjaga gaya LINQ yang lancar . Anda kemudian bisa melakukannya

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);
        }
    }
}
Sejuk
sumber
10
Secara pribadi, saya akan menamai metode ekstensi AndaToArrayAsync
torvin
4

Satu masalah dengan Task.WhenAll adalah hal itu akan membuat paralelisme. Dalam kebanyakan kasus mungkin lebih baik, tetapi terkadang Anda ingin menghindarinya. Misalnya, membaca data dalam batch dari DB dan mengirim data ke beberapa layanan web jarak jauh. Anda tidak ingin memuat semua kelompok ke memori tetapi menekan DB setelah kelompok sebelumnya telah diproses. Jadi, Anda harus mematahkan asinkronitas tersebut. Berikut ini contohnya:

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)
{
}

Catatan .GetAwaiter (). GetResult () mengonversinya menjadi sinkronisasi. DB akan dipukul secara malas hanya setelah batchSize peristiwa diproses.

Boris Lipschitz
sumber
1

Gunakan Task.WaitAllatau Task.WhenAllmana saja yang sesuai.

LB
sumber
1
Itu juga tidak berhasil. Task.WaitAllmemblokir, tidak dapat menunggu, dan tidak akan berfungsi dengan file Task<T>.
Matt Johnson-Pint
@MattJohnson WhenAll?
LB
Ya. Itu dia! Saya merasa bodoh. Terima kasih!
Matt Johnson-Pint
0

Task.WhenAll harus melakukan trik di sini.

Ameen
sumber