Menunggu beberapa Tugas dengan hasil yang berbeda

237

Saya punya 3 tugas:

private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}

Mereka semua perlu dijalankan sebelum kode saya dapat dilanjutkan dan saya juga membutuhkan hasil dari masing-masing. Tidak ada hasil yang memiliki kesamaan satu sama lain

Bagaimana cara saya menelepon dan menunggu 3 tugas selesai dan kemudian mendapatkan hasilnya?

Ian Vink
sumber
25
Apakah Anda memiliki persyaratan pemesanan? Artinya, apakah Anda ingin tidak menjual rumah sampai setelah kucing diberi makan?
Eric Lippert

Jawaban:

411

Setelah Anda gunakan WhenAll, Anda dapat menarik hasilnya secara individual dengan await:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Anda juga dapat menggunakan Task.Result(karena Anda tahu pada titik ini semuanya telah berhasil diselesaikan). Namun, saya sarankan menggunakan awaitkarena itu jelas benar, sementara Resultdapat menyebabkan masalah dalam skenario lain.

Stephen Cleary
sumber
83
Anda bisa menghapus WhenAllseluruhnya dari ini; menunggu akan memastikan Anda tidak melewati 3 tugas nanti sampai semua tugas selesai.
Servy
134
Task.WhenAll()memungkinkan untuk menjalankan tugas dalam mode paralel . Saya tidak mengerti mengapa @Servy menyarankan untuk menghapusnya. Tanpa WhenAllmereka akan dijalankan satu per satu
Sergey G.
87
@Sergey: Tugas mulai dijalankan segera. Misalnya, catTasksudah berjalan pada saat ia kembali FeedCat. Jadi salah satu pendekatan akan berhasil - satu-satunya pertanyaan adalah apakah Anda ingin awaitmereka satu per satu atau bersama-sama. Penanganan kesalahan sedikit berbeda - jika Anda menggunakan Task.WhenAll, maka semuanya akan ada await, bahkan jika salah satu dari mereka gagal lebih awal.
Stephen Cleary
23
@Sergey Calling WhenAlltidak berdampak pada saat operasi dijalankan, atau bagaimana mereka mengeksekusi. Ini hanya memiliki kemungkinan mempengaruhi bagaimana hasil diamati. Dalam kasus khusus ini, satu-satunya perbedaan adalah bahwa kesalahan dalam salah satu dari dua metode pertama akan menghasilkan pengecualian yang dilemparkan dalam tumpukan panggilan ini lebih awal dalam metode saya daripada Stephen (meskipun kesalahan yang sama akan selalu dilemparkan, jika ada ).
Servy
37
@Sergey: Kuncinya adalah bahwa metode asinkron selalu mengembalikan tugas "panas" (sudah dimulai).
Stephen Cleary
99

Hanya awaittiga tugas secara terpisah, setelah memulai semuanya.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;
Melayani
sumber
8
@Bargitta Tidak, itu salah. Mereka akan melakukan pekerjaan mereka secara paralel. Jangan ragu untuk menjalankannya dan lihat sendiri.
Servy
5
Orang-orang terus mengajukan pertanyaan yang sama setelah bertahun-tahun ... Saya merasa penting untuk menekankan lagi bahwa tugas " mulai dibuat " di tubuh jawabannya : mungkin mereka tidak repot membaca komentar
9
@StephenYork Menambahkan Task.WhenAllperubahan apa pun tentang perilaku program, dengan cara apa pun yang dapat diobservasi secara harfiah. Ini adalah pemanggilan metode yang benar-benar berlebihan. Anda boleh menambahkannya, jika Anda suka, sebagai pilihan estetika, tetapi itu tidak mengubah apa yang dilakukan kode. Waktu eksekusi kode akan identik dengan atau tanpa pemanggilan metode itu (well, secara teknis akan ada biaya overhead yang sangat kecil untuk menelepon WhenAll, tetapi ini harus diabaikan), hanya membuat versi itu sedikit lebih lama untuk dijalankan daripada versi ini.
Servy
4
@StephenYork Contoh Anda menjalankan operasi secara berurutan karena dua alasan. Metode asinkron Anda sebenarnya tidak asinkron, mereka sinkron. Fakta bahwa Anda memiliki metode sinkron yang selalu mengembalikan tugas yang sudah selesai mencegahnya berjalan secara bersamaan. Selanjutnya, Anda tidak benar-benar melakukan apa yang diperlihatkan dalam jawaban ini untuk memulai ketiga metode asinkron, dan kemudian menunggu ketiga tugas secara bergantian. Contoh Anda tidak memanggil setiap metode hingga yang sebelumnya selesai, sehingga secara eksplisit mencegah salah satu dari mulai sampai yang sebelumnya selesai, tidak seperti kode ini.
Servy
4
@MarcvanNieuwenhuijzen Itu terbukti tidak benar, seperti yang telah dibahas dalam komentar di sini, dan pada jawaban lain. Menambahkan WhenAlladalah perubahan estetika murni. Satu-satunya perbedaan yang dapat diamati dalam perilaku adalah apakah Anda menunggu tugas selanjutnya selesai jika tugas sebelumnya salah, yang biasanya tidak perlu dilakukan. Jika Anda tidak percaya dengan banyak penjelasan mengapa pernyataan Anda tidak benar, Anda dapat menjalankan sendiri kode dan melihat bahwa itu tidak benar.
Servy
37

Jika Anda menggunakan C # 7, Anda dapat menggunakan metode pembungkus berguna seperti ini ...

public static class TaskEx
{
    public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
    {
        return (await task1, await task2);
    }
}

... untuk mengaktifkan sintaks yang mudah digunakan seperti ini ketika Anda ingin menunggu beberapa tugas dengan jenis pengembalian yang berbeda. Anda harus membuat banyak kelebihan untuk sejumlah tugas yang berbeda untuk ditunggu, tentu saja.

var (someInt, someString) = await TaskEx.WhenAll(GetIntAsync(), GetStringAsync());

Namun, lihat jawaban Marc Gravell untuk beberapa optimasi di sekitar ValueTask dan tugas yang sudah selesai jika Anda bermaksud untuk mengubah contoh ini menjadi sesuatu yang nyata.

Joel Mueller
sumber
Tuples adalah satu-satunya fitur C # 7 yang terlibat di sini. Itu pasti dalam rilis final.
Joel Mueller
Saya tahu tentang tupel dan c # 7. Maksudku, aku tidak bisa menemukan metode WhenAll yang mengembalikan tupel. Namespace / paket apa?
Yury Scherbakov
@YuryShcherbakov Task.WhenAll()tidak mengembalikan tuple. Satu sedang dibangun dari Resultsifat - sifat tugas yang diberikan setelah tugas dikembalikan oleh Task.WhenAll()selesai.
Chris Charabaruk
2
Saya sarankan mengganti .Resulttelepon sesuai alasan Stephen untuk menghindari orang lain mengabadikan praktik buruk dengan menyalin contoh Anda.
julealgon
Saya bertanya-tanya mengapa metode ini bukan bagian dari kerangka kerja ini? Sepertinya sangat berguna. Apakah mereka kehabisan waktu dan harus berhenti pada tipe pengembalian tunggal?
Ian Grainger
14

Diberikan tiga tugas - FeedCat(), SellHouse()danBuyCar() , ada dua kasus menarik: apakah semuanya diselesaikan secara serempak (karena alasan tertentu, mungkin caching atau kesalahan), atau tidak.

Katakanlah kita punya, dari pertanyaan:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();
    // what here?
}

Sekarang, pendekatan sederhana adalah:

Task.WhenAll(x, y, z);

tapi ... itu tidak nyaman untuk memproses hasil; kami biasanya ingin awaititu:

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    await Task.WhenAll(x, y, z);
    // presumably we want to do something with the results...
    return DoWhatever(x.Result, y.Result, z.Result);
}

tetapi ini tidak banyak overhead dan mengalokasikan berbagai array (termasuk params Task[]array) dan daftar (secara internal). Ini bekerja, tetapi itu bukan IMO yang bagus. Dalam banyak hal lebih mudah untuk menggunakan asyncoperasi dan hanya awaitmasing - masing pada gilirannya:

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    // do something with the results...
    return DoWhatever(await x, await y, await z);
}

Berlawanan dengan beberapa komentar di atas, menggunakan awaitbukannya tidakTask.WhenAll membuat perbedaan pada bagaimana tugas dijalankan (secara bersamaan, secara berurutan, dll). Pada level tertinggi, Task.WhenAll mendahului dukungan compiler yang baik untuk async/ await, dan berguna ketika hal-hal itu tidak ada . Ini juga berguna ketika Anda memiliki array tugas yang sewenang-wenang, bukan 3 tugas yang tersembunyi.

Tapi: kita masih memiliki masalah yang async/ awaitmenghasilkan banyak compiler kebisingan untuk kelanjutan. Jika kemungkinan tugas tersebut benar-benar selesai secara sinkron, maka kami dapat mengoptimalkannya dengan membangun jalur sinkron dengan fallback asinkron:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    if(x.Status == TaskStatus.RanToCompletion &&
       y.Status == TaskStatus.RanToCompletion &&
       z.Status == TaskStatus.RanToCompletion)
        return Task.FromResult(
          DoWhatever(a.Result, b.Result, c.Result));
       // we can safely access .Result, as they are known
       // to be ran-to-completion

    return Awaited(x, y, z);
}

async Task Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
    return DoWhatever(await x, await y, await z);
}

Pendekatan "jalur sinkronisasi dengan async fallback" ini semakin umum terutama dalam kode kinerja tinggi di mana penyelesaian sinkron relatif sering. Perhatikan itu tidak akan membantu sama sekali jika penyelesaiannya selalu benar-benar tidak sinkron.

Hal-hal tambahan yang berlaku di sini:

  1. dengan C # baru-baru ini, pola umum untuk asyncmetode fallback umumnya diterapkan sebagai fungsi lokal:

    Task<string> DoTheThings() {
        async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        Task<Cat> x = FeedCat();
        Task<House> y = SellHouse();
        Task<Tesla> z = BuyCar();
    
        if(x.Status == TaskStatus.RanToCompletion &&
           y.Status == TaskStatus.RanToCompletion &&
           z.Status == TaskStatus.RanToCompletion)
            return Task.FromResult(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  2. lebih memilih ValueTask<T>untuk Task<T>jika ada kesempatan baik hal-hal yang pernah benar-benar serentak dengan banyak kembali nilai-nilai yang berbeda:

    ValueTask<string> DoTheThings() {
        async ValueTask<string> Awaited(ValueTask<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        ValueTask<Cat> x = FeedCat();
        ValueTask<House> y = SellHouse();
        ValueTask<Tesla> z = BuyCar();
    
        if(x.IsCompletedSuccessfully &&
           y.IsCompletedSuccessfully &&
           z.IsCompletedSuccessfully)
            return new ValueTask<string>(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  3. jika mungkin, lebih memilih IsCompletedSuccessfullyuntuk Status == TaskStatus.RanToCompletion; ini sekarang ada di .NET Core untuk Task, dan di mana-mana untukValueTask<T>

Marc Gravell
sumber
"Bertentangan dengan berbagai jawaban di sini, menggunakan menunggu daripada Task. Ketika semua tidak ada bedanya dengan bagaimana tugas berjalan (secara bersamaan, secara berurutan, dll)" Saya tidak melihat jawaban yang mengatakan itu. Saya sudah mengomentari mereka mengatakan banyak jika mereka melakukannya. Ada banyak komentar pada banyak jawaban yang mengatakan itu, tetapi tidak ada jawaban. Apa yang Anda maksud? Perhatikan juga bahwa jawaban Anda tidak menangani hasil tugas (atau berurusan dengan fakta bahwa hasilnya semua dari jenis yang berbeda). Anda telah mengomposisikan mereka dalam metode yang hanya mengembalikan Taskketika semuanya selesai tanpa menggunakan hasilnya.
Servy
@Servy Anda benar, itu komentar; Saya akan menambahkan tweak untuk menunjukkan menggunakan hasil
Marc Gravell
@Servy tweak menambahkan
Marc Gravell
Juga jika Anda akan mengambil langkah awal untuk menyerahkan tugas yang sinkron, Anda mungkin juga menangani tugas yang dibatalkan atau disinkronkan secara sinkron, daripada hanya yang berhasil diselesaikan. Jika Anda telah membuat keputusan bahwa ini adalah optimasi yang dibutuhkan oleh program Anda (yang akan jarang terjadi, tetapi akan terjadi) maka Anda bisa melakukannya.
Servy
@Servy itu adalah topik yang kompleks - Anda mendapatkan semantik pengecualian yang berbeda dari dua skenario - menunggu untuk memicu pengecualian berperilaku berbeda dari mengakses. Hasil untuk memicu pengecualian. IMO pada saat itu kita harus awaitmendapatkan semantik pengecualian "lebih baik", dengan asumsi bahwa pengecualian jarang tetapi bermakna
Marc Gravell
12

Anda dapat menyimpannya dalam tugas, lalu menunggu semuanya:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

Cat cat = await catTask;
House house = await houseTask;
Car car = await carTask;
Reed Copsey
sumber
tidak var catTask = FeedCat()melaksanakan fungsi FeedCat()dan menyimpan hasilnya ke dalam catTaskmembuat await Task.WhenAll()bagian jenis berguna karena metode ini telah dijalankan ??
Kraang Prime
1
@sanuel jika mereka mengembalikan tugas <t>, maka tidak ada ... mereka memulai async terbuka, tetapi jangan menunggu untuk itu
Reed Copsey
Saya tidak berpikir ini akurat, silakan lihat diskusi di bawah jawaban @ StephenCleary ... juga lihat jawaban Servy.
Rosdi Kasim
1
jika saya perlu menambahkan .ConfigrtueAwait (false). Apakah saya akan menambahkannya hanya ke Task.WhenAll atau ke setiap penunggu yang mengikuti?
AstroSharp
@AstroSharp secara umum, adalah ide yang bagus untuk menambahkannya ke mereka semua (jika yang pertama selesai, akan diabaikan secara efektif), tetapi dalam hal ini, mungkin akan baik-baik saja hanya melakukan yang pertama - kecuali ada lebih banyak async hal-hal terjadi kemudian.
Reed Copsey
6

Jika Anda mencoba untuk mencatat semua kesalahan, pastikan Anda tetap menggunakan Task.WhenAll baris dalam kode Anda, banyak komentar menyarankan bahwa Anda dapat menghapusnya dan menunggu tugas individu. Tugas. Ketika Semua sangat penting untuk penanganan kesalahan. Tanpa baris ini Anda berpotensi membiarkan kode Anda terbuka untuk pengecualian yang tidak teramati.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Bayangkan FeedCat melempar pengecualian dalam kode berikut:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Dalam hal ini Anda tidak akan pernah menunggu di houseTask atau carTask. Ada 3 skenario yang mungkin di sini:

  1. SellHouse sudah berhasil diselesaikan ketika FeedCat gagal. Dalam hal ini Anda baik-baik saja.

  2. SellHouse tidak lengkap dan gagal kecuali di beberapa titik. Pengecualian tidak diamati dan akan dipasang kembali pada utas finalizer.

  3. SellHouse tidak lengkap dan berisi menunggu di dalamnya. Jika kode Anda berjalan di ASP.NET SellHouse akan gagal segera setelah beberapa menunggu akan selesai di dalamnya. Ini terjadi karena Anda pada dasarnya membuat panggilan api & lupa dan konteks sinkronisasi hilang begitu FeedCat gagal.

Berikut adalah kesalahan yang akan Anda dapatkan untuk kasus (3):

System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()
   --- End of inner exception stack trace ---
---> (Inner Exception #0) System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()<---

Untuk kasus (2) Anda akan mendapatkan kesalahan yang sama tetapi dengan jejak stack pengecualian asli.

Untuk .NET 4.0 dan yang lebih baru, Anda dapat menangkap pengecualian yang tidak teramati menggunakan TaskScheduler.UnobservedTaskException. Untuk .NET 4.5 dan kemudian pengecualian yang tidak diobservasi ditelan secara default untuk .NET 4.0 pengecualian yang tidak diobservasi akan membuat proses Anda macet.

Lebih detail di sini: Penanganan Pengecualian Tugas di .NET 4.5

samfromlv
sumber
2

Anda dapat menggunakan Task.WhenAllseperti yang disebutkan, atau Task.WaitAll, tergantung pada apakah Anda ingin utas menunggu. Lihatlah tautan untuk penjelasan keduanya.

WaitAll vs WhenAll

christiandev
sumber
2

Gunakan Task.WhenAlldan kemudian tunggu hasilnya:

var tCat = FeedCat();
var tHouse = SellHouse();
var tCar = BuyCar();
await Task.WhenAll(tCat, tHouse, tCar);
Cat cat = await tCat;
House house = await tHouse;
Tesla car = await tCar; 
//as they have all definitely finished, you could also use Task.Value.
Itu adalah Notalie.
sumber
mm ... bukan Task.Value (mungkin dulu ada pada 2013?), lebih tepatnya tCat.Result, tHouse.Result atau tCar.Result
Stephen York
1

Peringatan ke Depan

Hanya kepala cepat ke mereka yang mengunjungi ini dan utas serupa lainnya mencari cara untuk memparalelkan EntityFramework menggunakan async + menunggu + task tool-set : Namun, pola yang ditunjukkan di sini adalah suara, ketika menyangkut kepingan salju EF Anda tidak akan mencapai eksekusi paralel kecuali dan sampai Anda menggunakan instance db-context-instance yang terpisah di dalam masing-masing dan setiap panggilan * Async () yang terlibat.

Hal semacam ini diperlukan karena keterbatasan desain inheren konteks ef-db yang melarang menjalankan beberapa query secara paralel dalam instance konteks-ef yang sama.


Dengan memanfaatkan jawaban yang telah diberikan, ini adalah cara untuk memastikan bahwa Anda mengumpulkan semua nilai bahkan dalam kasus satu atau lebih tugas menghasilkan pengecualian:

  public async Task<string> Foobar() {
    async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
        return DoSomething(await a, await b, await c);
    }

    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        if (carTask.Status == TaskStatus.RanToCompletion //triple
            && catTask.Status == TaskStatus.RanToCompletion //cache
            && houseTask.Status == TaskStatus.RanToCompletion) { //hits
            return Task.FromResult(DoSomething(catTask.Result, carTask.Result, houseTask.Result)); //fast-track
        }

        cat = await catTask;
        car = await carTask;
        house = await houseTask;
        //or Task.AwaitAll(carTask, catTask, houseTask);
        //or await Task.WhenAll(carTask, catTask, houseTask);
        //it depends on how you like exception handling better

        return Awaited(catTask, carTask, houseTask);
   }
 }

Implementasi alternatif yang kurang lebih memiliki karakteristik kinerja yang sama dapat berupa:

 public async Task<string> Foobar() {
    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        cat = catTask.Status == TaskStatus.RanToCompletion ? catTask.Result : (await catTask);
        car = carTask.Status == TaskStatus.RanToCompletion ? carTask.Result : (await carTask);
        house = houseTask.Status == TaskStatus.RanToCompletion ? houseTask.Result : (await houseTask);

        return DoSomething(cat, car, house);
     }
 }
XDS
sumber
-1
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

jika Anda ingin mengakses Cat, lakukan ini:

var ct = (Cat)dn[0];

Ini sangat mudah dilakukan dan sangat berguna untuk digunakan, tidak perlu mencari solusi yang kompleks.

taurius
sumber
1
Hanya ada satu masalah dengan ini: dynamicadalah iblis. Ini untuk interop COM yang rumit dan semacamnya, dan tidak boleh digunakan dalam situasi apa pun di mana itu tidak benar-benar diperlukan. Terutama jika Anda peduli dengan kinerja. Atau ketik keamanan. Atau refactoring. Atau debugging.
Joel Mueller