Async tunggu di linq pilih

180

Saya perlu memodifikasi program yang sudah ada dan berisi kode berikut:

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

Tetapi ini terasa sangat aneh bagi saya, pertama-tama penggunaan asyncdan awaitdalam pemilihan. Menurut jawaban Stephen Cleary ini, saya seharusnya bisa menghilangkannya.

Lalu yang kedua Selectyang memilih hasilnya. Bukankah ini berarti tugas sama sekali bukan asinkron dan dilakukan secara serempak (begitu banyak usaha sia-sia), atau akankah tugas dilakukan secara serempak dan ketika selesai, sisa permintaan dieksekusi?

Haruskah saya menulis kode di atas seperti mengikuti menurut jawaban lain oleh Stephen Cleary :

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

dan apakah ini sama seperti ini?

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

Sementara saya sedang mengerjakan proyek ini saya ingin mengubah contoh kode pertama tetapi saya tidak terlalu tertarik untuk mengubah kode async (tampaknya bekerja). Mungkin saya hanya mengkhawatirkan apa-apa dan ketiga sampel kode melakukan hal yang persis sama?

ProcessEventsAsync terlihat seperti ini:

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}
Alexander Derck
sumber
Apa jenis pengembalian ProceesEventAsync?
tede24
@ tede24 Ini Task<InputResult>dengan InputResultmenjadi kelas khusus.
Alexander Derck
Versi Anda jauh lebih mudah dibaca menurut saya. Namun, Anda sudah lupa dengan Selecthasil dari tugas sebelum Anda Where.
Maks
Dan InputResult memiliki properti Hasil, bukan?
tede24
@ tede24 Hasil adalah milik tugas bukan kelas saya. Dan @Max yang menunggu harus memastikan saya mendapatkan hasil tanpa mengakses Resultproperti tugas
Alexander Derck

Jawaban:

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

Tapi ini terasa sangat aneh bagi saya, pertama-tama penggunaan async dan menunggu di pilih. Menurut jawaban Stephen Cleary ini, saya seharusnya bisa menghilangkannya.

Panggilan ke Selectvalid. Kedua garis ini pada dasarnya identik:

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

(Ada sedikit perbedaan tentang bagaimana pengecualian sinkron akan dilempar dari ProcessEventAsync, tetapi dalam konteks kode ini tidak masalah sama sekali.)

Kemudian Pilih kedua yang memilih hasilnya. Bukankah ini berarti tugasnya sama sekali tidak async dan dilakukan secara serempak (begitu banyak usaha sia-sia), atau akankah tugas itu dilakukan secara serempak dan ketika selesai, sisa permintaan dieksekusi?

Ini berarti bahwa kueri sedang memblokir. Jadi itu tidak benar-benar tidak sinkron.

Hancurkan:

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

pertama-tama akan memulai operasi asinkron untuk setiap peristiwa. Maka baris ini:

                   .Select(t => t.Result)

akan menunggu operasi tersebut selesai satu per satu (pertama menunggu operasi acara pertama, lalu berikutnya, lalu berikutnya, dll).

Ini adalah bagian yang saya tidak peduli, karena itu memblokir dan juga akan membungkus pengecualian AggregateException.

dan apakah ini sama seperti ini?

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();

Ya, kedua contoh itu setara. Keduanya memulai semua operasi asinkron (events.Select(...) ), lalu menunggu secara tak sinkron untuk semua operasi selesai dalam urutan apa pun ( await Task.WhenAll(...)), kemudian melanjutkan dengan sisa pekerjaan ( Where...).

Kedua contoh ini berbeda dari kode aslinya. Kode asli diblokir dan akan membungkus pengecualian dalam AggregateException.

Stephen Cleary
sumber
Sorakan untuk membersihkan itu! Jadi, alih-alih pengecualian yang dimasukkan ke dalam AggregateExceptionsaya akan mendapatkan beberapa pengecualian terpisah dalam kode kedua?
Alexander Derck
1
@AlexanderDerck: Tidak, baik di kode lama dan baru, hanya pengecualian pertama yang dimunculkan. Tetapi dengan Resultitu akan dibungkus AggregateException.
Stephen Cleary
Saya mendapatkan jalan buntu di Kontroler ASP.NET MVC saya menggunakan kode ini. Saya menyelesaikannya menggunakan Task.Run (...). Saya tidak memiliki perasaan yang baik tentang hal itu. Namun, itu selesai tepat ketika menjalankan tes async xUnit. Apa yang sedang terjadi?
SuperJMN
2
@ SupupJMN: Ganti stuff.Select(x => x.Result);denganawait Task.WhenAll(stuff)
Stephen Cleary
1
@Aniel: Mereka pada dasarnya sama. Ada beberapa perbedaan seperti mesin negara, menangkap konteks, perilaku pengecualian sinkron. Info lebih lanjut di blog.stephencleary.com/2016/12/eliding-async-await.html
Stephen Cleary
25

Kode yang ada berfungsi, tetapi memblokir utas.

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

membuat Tugas baru untuk setiap acara, tetapi

.Select(t => t.Result)

memblokir utas menunggu setiap tugas baru berakhir.

Di sisi lain kode Anda menghasilkan hasil yang sama tetapi tetap tidak sinkron.

Hanya satu komentar pada kode pertama Anda. Garis ini

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

akan menghasilkan Tugas tunggal sehingga variabel harus dinamai dalam singular.

Akhirnya kode terakhir Anda menghasilkan hal yang sama tetapi lebih ringkas

Untuk referensi: Task.Wait / Task.WhenAll

tede24
sumber
Jadi blok kode pertama sebenarnya dijalankan secara sinkron?
Alexander Derck
1
Ya, karena mengakses Hasil menghasilkan Tunggu yang memblokir utas. Di sisi lain Saat menghasilkan Tugas baru yang dapat Anda tunggu.
tede24
1
Kembali ke pertanyaan ini dan melihat komentar Anda tentang nama tasksvariabel, Anda sepenuhnya benar. Pilihan yang mengerikan, mereka bahkan bukan tugas karena mereka segera ditunggu. Saya hanya akan meninggalkan pertanyaan seperti itu
Alexander Derck
13

Dengan metode saat ini tersedia di Linq terlihat sangat jelek:

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();

Semoga versi berikut. NET akan datang dengan tooling yang lebih elegan untuk menangani koleksi tugas dan tugas koleksi.

Vitaliy Ulantikov
sumber
12

Saya menggunakan kode ini:

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

seperti ini:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params));
Siderite Zackwehdex
sumber
5
Ini hanya membungkus fungsi yang ada dengan cara yang lebih tidak jelas
Alexander Derck
Alternatifnya adalah var result = menunggu Task.WhenAll (sourceEnumerable.Select (async s => menunggu someFunction (s, params lainnya)). Ia berfungsi juga, tapi itu bukan LINQy
Siderite Zackwehdex
Bukankah seharusnya Func<TSource, Task<TResult>> methodberisi yang other paramsdisebutkan pada bit kode kedua?
matramos
2
Parameter tambahan bersifat eksternal, tergantung pada fungsi yang ingin saya jalankan, mereka tidak relevan dalam konteks metode ekstensi.
Siderite Zackwehdex
5
Itu metode ekstensi yang bagus. Tidak yakin mengapa itu dianggap "lebih tidak jelas" - secara analog analog dengan sinkron Select(), jadi drop-in yang elegan.
nullPainter
11

Saya lebih suka ini sebagai metode ekstensi:

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

Sehingga dapat digunakan dengan metode chaining:

var inputs = await events
  .Select(async ev => await ProcessEventAsync(ev))
  .WhenAll()
Daryl
sumber
1
Anda seharusnya tidak memanggil metode Waititu ketika sebenarnya tidak menunggu. Itu menciptakan tugas yang selesai ketika semua tugas selesai. Sebut saja WhenAll, seperti Taskmetode yang ditiru. Tidak ada gunanya juga metode ini async. Panggil saja WhenAlldan selesaikan saja.
Servy
Sedikit pembungkus yang tidak berguna menurut saya ketika itu hanya memanggil metode asli
Alexander Derck
@Servy fair point, tapi saya khususnya tidak suka opsi nama apa pun. WhenAll membuatnya terdengar seperti sebuah acara yang tidak cukup.
Daryl
3
@AlexanderDerck keuntungannya adalah Anda dapat menggunakannya dalam metode chaining.
Daryl
1
@Daryl karena WhenAllmengembalikan daftar yang dievaluasi (tidak dievaluasi dengan malas), argumen dapat dibuat untuk menggunakan Task<T[]>jenis pengembalian untuk menandakan itu. Ketika ditunggu, ini masih bisa menggunakan Linq, tetapi juga mengomunikasikan bahwa itu tidak malas.
JAD