Mengapa saya harus lebih memilih 'await Task.WhenAll' tunggal daripada beberapa menunggu?

127

Jika saya tidak peduli dengan urutan penyelesaian tugas dan hanya ingin semuanya diselesaikan, haruskah saya tetap menggunakan await Task.WhenAllalih-alih beberapa await? misalnya, apakah di DoWork2bawah metode yang disukai DoWork1(dan mengapa?):

using System;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Program
    {
        static async Task<string> DoTaskAsync(string name, int timeout)
        {
            var start = DateTime.Now;
            Console.WriteLine("Enter {0}, {1}", name, timeout);
            await Task.Delay(timeout);
            Console.WriteLine("Exit {0}, {1}", name, (DateTime.Now - start).TotalMilliseconds);
            return name;
        }

        static async Task DoWork1()
        {
            var t1 = DoTaskAsync("t1.1", 3000);
            var t2 = DoTaskAsync("t1.2", 2000);
            var t3 = DoTaskAsync("t1.3", 1000);

            await t1; await t2; await t3;

            Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }

        static async Task DoWork2()
        {
            var t1 = DoTaskAsync("t2.1", 3000);
            var t2 = DoTaskAsync("t2.2", 2000);
            var t3 = DoTaskAsync("t2.3", 1000);

            await Task.WhenAll(t1, t2, t3);

            Console.WriteLine("DoWork2 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }


        static void Main(string[] args)
        {
            Task.WhenAll(DoWork1(), DoWork2()).Wait();
        }
    }
}
avo
sumber
2
Bagaimana jika Anda tidak benar-benar tahu berapa banyak tugas yang perlu Anda lakukan secara paralel? Bagaimana jika Anda memiliki 1000 tugas yang perlu dijalankan? Yang pertama tidak akan banyak terbaca await t1; await t2; ....; await tn=> yang kedua selalu merupakan pilihan terbaik dalam kedua kasus
cuongle
Komentar Anda masuk akal. Saya hanya mencoba mengklarifikasi sesuatu untuk diri saya sendiri, terkait pertanyaan lain yang baru-baru ini saya jawab . Dalam hal ini, ada 3 tugas.
avo

Jawaban:

113

Ya, gunakan WhenAllkarena menyebarkan semua kesalahan sekaligus. Dengan beberapa menunggu, Anda kehilangan kesalahan jika salah satu dari yang sebelumnya menunggu lemparan.

Perbedaan penting lainnya adalah bahwa WhenAllakan menunggu semua tugas selesai bahkan saat ada kegagalan (tugas yang salah atau dibatalkan). Menunggu secara manual secara berurutan akan menyebabkan konkurensi yang tidak terduga karena bagian dari program Anda yang ingin menunggu sebenarnya akan berlanjut lebih awal.

Saya rasa itu juga membuat membaca kode lebih mudah karena semantik yang Anda inginkan langsung didokumentasikan dalam kode.

usr
sumber
9
"Karena itu menyebarkan semua kesalahan sekaligus" Tidak jika Anda awaithasilnya.
svick
2
Adapun pertanyaan tentang bagaimana pengecualian dikelola dengan Task, artikel ini memberikan wawasan cepat tapi bagus untuk alasan di baliknya (dan kebetulan juga membuat catatan singkat tentang manfaat WhenAll dibandingkan dengan beberapa menunggu): blog .msdn.com / b / pfxteam / archive / 2011/09/28 / 10217876.aspx
Oskar Lindberg
5
@OskarLindberg, OP memulai semua tugas sebelum dia menunggu yang pertama. Jadi mereka berjalan secara bersamaan. Terima kasih untuk tautannya.
usr
3
@usr Saya masih penasaran ingin tahu apakah WhenAll tidak melakukan hal-hal pintar seperti melestarikan SynchronizationContext yang sama, untuk lebih mendorong manfaatnya selain dari semantik. Saya tidak menemukan dokumentasi yang meyakinkan, tetapi melihat pada IL terdapat implementasi IAsyncStateMachine yang berbeda. Saya tidak membaca IL dengan baik, tetapi WhenAll setidaknya tampaknya menghasilkan kode IL yang lebih efisien. (Bagaimanapun, fakta saja bahwa hasil WhenAll mencerminkan keadaan semua tugas yang terlibat bagi saya adalah alasan yang cukup untuk memilihnya dalam banyak kasus.)
Oskar Lindberg
17
Perbedaan penting lainnya adalah WhenAll akan menunggu semua tugas selesai, bahkan jika, misalnya, t1 atau t2 melontarkan pengecualian atau dibatalkan.
Magnus
28

Pemahaman saya adalah bahwa alasan utama untuk memilih Task.WhenAllke beberapa awaits adalah kinerja / tugas "berputar": the DoWork1metode melakukan sesuatu seperti ini:

  • mulai dengan konteks tertentu
  • simpan konteksnya
  • tunggu t1
  • mengembalikan konteks aslinya
  • simpan konteksnya
  • tunggu t2
  • mengembalikan konteks aslinya
  • simpan konteksnya
  • tunggu t3
  • mengembalikan konteks aslinya

Sebaliknya, DoWork2lakukan ini:

  • mulai dengan konteks tertentu
  • simpan konteksnya
  • tunggu semua t1, t2 dan t3
  • mengembalikan konteks aslinya

Apakah ini masalah yang cukup besar untuk kasus khusus Anda, tentu saja, "bergantung pada konteks" (maafkan permainan kata).

Marcel Popescu
sumber
4
Anda tampaknya berpikir bahwa mengirim pesan ke konteks sinkronisasi itu mahal. Benar-benar tidak. Anda memiliki delegasi yang ditambahkan ke antrian, antrian itu akan dibaca dan delegasi tersebut dieksekusi. Overhead yang ditambahkan ini sejujurnya sangat kecil. Bukan apa-apa, tapi juga tidak besar. Biaya operasi asinkron apa pun akan mengecilkan overhead seperti itu di hampir semua kasus.
Pelayanan
Setuju, itu hanya satu-satunya alasan yang terpikir olehku untuk memilih salah satu daripada yang lain. Nah, itu ditambah kemiripan dengan Task.WaitAll di mana peralihan utas adalah biaya yang lebih signifikan.
Marcel Popescu
1
@Seperti yang ditunjukkan Marry bahwa BENAR-BENAR tergantung. Jika Anda menggunakan await pada semua tugas db sebagai masalah prinsip misalnya, dan db itu berada di mesin yang sama dengan instance asp.net, ada kasus Anda akan menunggu db hit yang ada di dalam memori dalam indeks, lebih murah selain itu sakelar sinkronisasi dan pengocokan threadpool. Mungkin ada kemenangan keseluruhan yang signifikan dengan WhenAll () dalam skenario semacam itu, jadi ... itu sangat tergantung.
Chris Moschini
3
@ChrisMoschini Tidak mungkin query DB, meskipun mengenai DB yang berada di mesin yang sama dengan server, akan menjadi lebih cepat daripada overhead penambahan beberapa delegasi ke pompa pesan. Permintaan dalam memori itu hampir pasti akan menjadi jauh lebih lambat.
Pelayanan
Perhatikan juga bahwa jika t1 lebih lambat dan t2 dan t3 lebih cepat - maka yang lain menunggu segera kembali.
David Refaeli
18

Metode asinkron diimplementasikan sebagai mesin negara. Dimungkinkan untuk menulis metode sehingga tidak dikompilasi ke dalam mesin-negara, ini sering disebut sebagai metode asinkron jalur cepat. Ini dapat diterapkan seperti ini:

public Task DoSomethingAsync()
{
    return DoSomethingElseAsync();
}

Saat menggunakannya Task.WhenAll, dimungkinkan untuk mempertahankan kode jalur cepat ini sambil tetap memastikan pemanggil dapat menunggu hingga semua tugas diselesaikan, misalnya:

public Task DoSomethingAsync()
{
    var t1 = DoTaskAsync("t2.1", 3000);
    var t2 = DoTaskAsync("t2.2", 2000);
    var t3 = DoTaskAsync("t2.3", 1000);

    return Task.WhenAll(t1, t2, t3);
}
Lukazoid
sumber
7

(Penafian: Jawaban ini diambil / terinspirasi dari kursus Async TPL Ian Griffiths di Pluralsight )

Alasan lain untuk memilih WhenAll adalah penanganan Exception.

Misalkan Anda memiliki blok coba-tangkap pada metode DoWork Anda, dan anggaplah metode tersebut memanggil metode DoTask yang berbeda:

static async Task DoWork1() // modified with try-catch
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        await t1; await t2; await t3;

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
    }
    catch (Exception x)
    {
        // ...
    }

}

Dalam kasus ini, jika ketiga tugas menampilkan pengecualian, hanya tugas pertama yang akan ditangkap. Pengecualian selanjutnya akan hilang. Yaitu jika t2 dan t3 mengeluarkan eksepsi, hanya t2 yang akan ditangkap; dll. Pengecualian tugas berikutnya tidak akan teramati.

Dimana seperti di WhenAll - jika ada atau semua tugas gagal, tugas yang dihasilkan akan berisi semua pengecualian. Kata kunci await masih selalu memunculkan kembali pengecualian pertama. Jadi pengecualian lainnya masih belum teramati secara efektif. Salah satu cara untuk mengatasinya adalah dengan menambahkan lanjutan kosong setelah tugas WhenAll dan meletakkan menunggu di sana. Dengan cara ini jika tugas gagal, properti result akan menampilkan Pengecualian Gabungan penuh:

static async Task DoWork2() //modified to catch all exceptions
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        var t = Task.WhenAll(t1, t2, t3);
        await t.ContinueWith(x => { });

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t.Result[0], t.Result[1], t.Result[2]));
    }
    catch (Exception x)
    {
        // ...
    }
}
David Refaeli
sumber
6

Jawaban lain untuk pertanyaan ini menawarkan alasan teknis mengapa await Task.WhenAll(t1, t2, t3);lebih disukai. Jawaban ini akan bertujuan untuk melihatnya dari sisi yang lebih lembut (yang disinggung @usr) sambil tetap sampai pada kesimpulan yang sama.

await Task.WhenAll(t1, t2, t3); adalah pendekatan yang lebih fungsional, karena mendeklarasikan maksud dan atom.

Dengan await t1; await t2; await t3;, tidak ada yang mencegah rekan setimnya (atau mungkin bahkan masa depan Anda sendiri!) Dari menambahkan kode antara individu awaitpernyataan. Tentu, Anda telah mengompresnya menjadi satu baris untuk mencapai itu, tetapi itu tidak menyelesaikan masalah. Selain itu, umumnya merupakan bentuk yang buruk dalam pengaturan tim untuk menyertakan beberapa pernyataan pada baris kode tertentu, karena dapat membuat file sumber lebih sulit untuk dipindai oleh mata manusia.

Sederhananya, await Task.WhenAll(t1, t2, t3);lebih mudah dipelihara, karena mengkomunikasikan maksud Anda dengan lebih jelas dan tidak terlalu rentan terhadap bug aneh yang dapat muncul dari pembaruan kode yang bermaksud baik, atau bahkan hanya menggabungkan kesalahan.

rarrarrarrr
sumber