Bagikan melalui


Pemrograman asinkron dengan async dan await

Model Task asynchronous programming (TAP) menyediakan lapisan abstraksi atas pengkodan asinkron yang khas. Dalam model ini, Anda menulis kode sebagai urutan pernyataan, sama seperti biasa. Perbedaannya adalah Anda dapat membaca kode berbasis tugas saat pengkompilasi memproses setiap pernyataan dan sebelum mulai memproses pernyataan berikutnya. Untuk mencapai model ini, pengkompilasi melakukan banyak transformasi untuk menyelesaikan setiap tugas. Beberapa pernyataan dapat memulai pekerjaan dan mengembalikan objek Task yang mewakili pekerjaan yang sedang berlangsung dan pengkompilasi harus menyelesaikan transformasi ini. Tujuan dari pemrograman tugas asinkron adalah untuk memungkinkan kode yang dapat dibaca seperti urutan pernyataan, tetapi dijalankan dalam urutan yang lebih rumit. Eksekusi didasarkan pada alokasi sumber daya eksternal dan ketika tugas selesai.

Model pemrograman asinkron tugas dianalogikan dengan bagaimana orang memberikan instruksi untuk proses yang menyertakan tugas asinkron. Artikel ini menggunakan contoh dengan instruksi untuk membuat sarapan untuk menunjukkan bagaimana kata kunci async dan await mempermudah alasan tentang kode yang mencakup serangkaian instruksi asinkron. Instruksi untuk membuat sarapan mungkin disediakan sebagai daftar:

  1. Tuangkan secangkir kopi.
  2. Panaskan wajan, lalu goreng dua butir telur.
  3. Masak tiga potongan hash brown.
  4. Panggang dua potong roti.
  5. Sebarkan mentega dan selai di roti panggang.
  6. Tuangkan segelas jus jeruk.

Jika Anda memiliki pengalaman dengan memasak, Anda mungkin menyelesaikan instruksi ini secara asinkron. Anda mulai menghangatkan panci untuk telur, lalu mulai memasak hash brown. Anda menaruh roti di pemanggang roti, lalu mulai memasak telur. Pada setiap langkah proses, Anda memulai tugas, lalu beralih ke tugas lain yang siap untuk anda perhatikan.

Memasak sarapan adalah contoh yang baik dari pekerjaan asinkron yang tidak paralel. Satu orang (atau utas) dapat menangani semua tugas. Satu orang dapat membuat sarapan secara asinkron dengan memulai tugas berikutnya sebelum tugas sebelumnya selesai. Setiap tugas memasak berlangsung terlepas dari apakah seseorang secara aktif menonton prosesnya. Segera setelah Anda mulai menghangatkan wajan untuk telur, Anda dapat mulai memasak rostis. Setelah hash brown mulai dimasak, Anda dapat memasukkan roti ke dalam pemanggang roti.

Untuk algoritma paralel, Anda memerlukan beberapa orang yang memasak (atau beberapa thread). Satu orang memasak telur, yang lain memasak hash browns, dan sebagainya. Setiap orang berfokus pada satu tugas tertentu. Setiap orang yang memasak (atau setiap utas) diblokir menunggu secara sinkron hingga tugas saat ini selesai: Hash browns siap dibalik, roti siap muncul dari pemanggang roti, dan sebagainya.

Diagram yang menunjukkan instruksi untuk menyiapkan sarapan sebagai daftar tujuh tugas berurutan yang diselesaikan dalam 30 menit.

Pertimbangkan daftar instruksi sinkron yang sama yang ditulis sebagai pernyataan kode C#:

using System;
using System.Threading.Tasks;

namespace AsyncBreakfast
{
    // These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
    internal class HashBrown { }
    internal class Coffee { }
    internal class Egg { }
    internal class Juice { }
    internal class Toast { }

    class Program
    {
        static void Main(string[] args)
        {
            Coffee cup = PourCoffee();
            Console.WriteLine("coffee is ready");

            Egg eggs = FryEggs(2);
            Console.WriteLine("eggs are ready");

            HashBrown hashBrown = FryHashBrowns(3);
            Console.WriteLine("hash browns are ready");

            Toast toast = ToastBread(2);
            ApplyButter(toast);
            ApplyJam(toast);
            Console.WriteLine("toast is ready");

            Juice oj = PourOJ();
            Console.WriteLine("oj is ready");
            Console.WriteLine("Breakfast is ready!");
        }

        private static Juice PourOJ()
        {
            Console.WriteLine("Pouring orange juice");
            return new Juice();
        }

        private static void ApplyJam(Toast toast) =>
            Console.WriteLine("Putting jam on the toast");

        private static void ApplyButter(Toast toast) =>
            Console.WriteLine("Putting butter on the toast");

        private static Toast ToastBread(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("Putting a slice of bread in the toaster");
            }
            Console.WriteLine("Start toasting...");
            Task.Delay(3000).Wait();
            Console.WriteLine("Remove toast from toaster");

            return new Toast();
        }

        private static HashBrown FryHashBrowns(int patties)
        {
            Console.WriteLine($"putting {patties} hash brown patties in the pan");
            Console.WriteLine("cooking first side of hash browns...");
            Task.Delay(3000).Wait();
            for (int patty = 0; patty < patties; patty++)
            {
                Console.WriteLine("flipping a hash brown patty");
            }
            Console.WriteLine("cooking the second side of hash browns...");
            Task.Delay(3000).Wait();
            Console.WriteLine("Put hash browns on plate");

            return new HashBrown();
        }

        private static Egg FryEggs(int howMany)
        {
            Console.WriteLine("Warming the egg pan...");
            Task.Delay(3000).Wait();
            Console.WriteLine($"cracking {howMany} eggs");
            Console.WriteLine("cooking the eggs ...");
            Task.Delay(3000).Wait();
            Console.WriteLine("Put eggs on plate");

            return new Egg();
        }

        private static Coffee PourCoffee()
        {
            Console.WriteLine("Pouring coffee");
            return new Coffee();
        }
    }
}

Jika Anda menafsirkan instruksi ini seperti komputer, sarapan membutuhkan waktu sekitar 30 menit untuk disiapkan. Durasinya adalah jumlah waktu tugas individual. Komputer memblokir untuk setiap pernyataan hingga semua pekerjaan selesai, dan kemudian melanjutkan ke pernyataan tugas berikutnya. Pendekatan ini dapat memakan waktu yang signifikan. Dalam contoh sarapan, metode komputer menciptakan sarapan yang tidak memuaskan. Tugas selanjutnya dalam daftar sinkron, seperti memanggang roti, jangan mulai sampai tugas sebelumnya selesai. Beberapa makanan menjadi dingin sebelum sarapan siap disajikan.

Jika Anda ingin komputer menjalankan instruksi secara asinkron, Anda harus menulis kode asinkron. Saat Anda menulis program klien, Anda ingin UI responsif terhadap input pengguna. Aplikasi Anda tidak boleh membekukan semua interaksi saat mengunduh data dari web. Ketika Anda menulis program server, Anda tidak ingin memblokir utas yang mungkin melayani permintaan lain. Menggunakan kode sinkron ketika ada alternatif asinkron dapat menghambat kemampuan Anda untuk meningkatkan skala dengan biaya yang lebih hemat. Anda membayar utas yang diblokir.

Aplikasi modern yang berhasil memerlukan kode asinkron. Tanpa dukungan bahasa, menulis kode asinkron memerlukan panggilan balik, peristiwa penyelesaian, atau cara lain yang mengaburkan niat asli kode. Keuntungan dari kode sinkron adalah tindakan langkah demi langkah yang memudahkan untuk memindai dan memahami. Model asinkron tradisional memaksa Anda untuk fokus pada sifat kode asinkron, bukan pada tindakan dasar kode.

Jangan blokir, tunggu saja

Kode sebelumnya menyoroti praktik pemrograman yang tidak menguntungkan: Menulis kode sinkron untuk melakukan operasi asinkron. Kode memblokir thread saat ini dari melakukan pekerjaan lain. Kode tidak akan mengganggu utas saat ada tugas yang sedang berjalan. Hasil dari model ini mirip dengan menatap pemangas roti setelah Anda memasukkan roti. Anda mengabaikan gangguan apa pun dan tidak memulai tugas lain sampai roti muncul. Anda tidak mengambil mentega dan selai keluar dari lemari es. Anda mungkin melewatkan melihat ketika api mulai menyala di atas kompor. Anda ingin memanggang roti sambil menangani masalah lain pada saat yang sama. Hal yang sama berlaku dengan kode Anda.

Anda dapat memulai dengan memperbarui kode sehingga thread tidak terblokir saat tugas sedang berjalan. Kata kunci await menyediakan cara nonblokir untuk memulai tugas, lalu melanjutkan eksekusi saat tugas selesai. Versi asinkron sederhana dari kode sarapan terlihat seperti cuplikan berikut:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");

    Egg eggs = await FryEggsAsync(2);
    Console.WriteLine("eggs are ready");

    HashBrown hashBrown = await FryHashBrownsAsync(3);
    Console.WriteLine("hash browns are ready");

    Toast toast = await ToastBreadAsync(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");

    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");
    Console.WriteLine("Breakfast is ready!");
}

Kode memperbarui badan metode asli dari FryEggs, FryHashBrowns, dan ToastBread untuk mengembalikan objek Task<Egg>, Task<HashBrown>, dan Task<Toast> masing-masing. Nama metode yang diperbarui mencakup akhiran "Asinkron": FryEggsAsync, , FryHashBrownsAsyncdan ToastBreadAsync. Metode Main mengembalikan objek Task, meskipun tidak memiliki ekspresi return, yaitu berdasarkan desain. Untuk informasi selengkapnya, lihat evaluasi fungsi asinkron yang mengembalikan kekosongan.

Nota

Kode yang diperbarui belum memanfaatkan fitur utama pemrograman asinkron, yang dapat mengakibatkan waktu penyelesaian yang lebih singkat. Kode memproses tugas dalam jumlah waktu yang kira-kira sama dengan versi sinkron awal. Untuk implementasi metode lengkap, lihat versi akhir kode nanti di artikel ini.

Mari kita terapkan contoh sarapan ke kode yang diperbarui. Utas tidak memblokir saat telur atau hash brown sedang dimasak, tetapi kode juga tidak memulai tugas lain sampai pekerjaan saat ini selesai. Anda masih menaruh roti di pemanggang roti dan menatap pemanggang roti sampai roti muncul, tetapi Anda sekarang dapat merespons gangguan. Di restoran tempat beberapa pesanan ditempatkan, juru masak dapat memulai pesanan baru sementara yang lain sudah memasak.

Dalam kode yang diperbarui, utas yang mengerjakan sarapan tidak diblokir saat menunggu tugas yang sudah dimulai tetapi belum selesai. Untuk beberapa aplikasi, perubahan ini adalah yang Anda butuhkan. Anda dapat mengaktifkan aplikasi untuk mendukung interaksi pengguna saat unduhan data dari web. Dalam skenario lain, Anda mungkin ingin memulai tugas lain sambil menunggu tugas sebelumnya selesai.

Mulai tugas secara bersamaan

Untuk sebagian besar operasi, Anda ingin segera memulai beberapa tugas independen. Setelah setiap tugas selesai, Anda memulai pekerjaan lain yang siap dimulai. Ketika Anda menerapkan metodologi ini ke contoh sarapan, Anda dapat menyiapkan sarapan lebih cepat. Anda juga menyiapkan semuanya dekat dengan waktu yang sama, sehingga Anda dapat menikmati sarapan panas.

Kelas System.Threading.Tasks.Task dan jenis terkait adalah kelas yang dapat Anda gunakan untuk menerapkan gaya penalaran ini ke tugas yang sedang berlangsung. Pendekatan ini memungkinkan Anda menulis kode yang lebih mirip dengan cara Anda membuat sarapan dalam kehidupan nyata. Anda mulai memasak telur, hash brown, dan roti bakar pada saat yang sama. Karena setiap item makanan memerlukan tindakan, Anda memusatkan perhatian Anda pada tugas tersebut, menyelesaikan tindakan tersebut, dan kemudian menunggu sesuatu yang membutuhkan perhatian Anda.

Dalam kode, Anda memulai tugas dan berpegang pada objek Task yang mewakili pekerjaan. Anda menggunakan metode await pada tugas untuk menunda tindakan pada pekerjaan hingga hasilnya siap.

Terapkan perubahan ini pada kode sarapan. Langkah pertama adalah menyimpan tugas untuk operasi ketika tugas dimulai, daripada menggunakan ekspresi await:

Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");

Task<Egg> eggsTask = FryEggsAsync(2);
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");

Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");

Task<Toast> toastTask = ToastBreadAsync(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");

Juice oj = PourOJ();
Console.WriteLine("Oj is ready");
Console.WriteLine("Breakfast is ready!");

Revisi ini tidak membantu menyiapkan sarapan Anda lebih cepat. Ekspresi await diterapkan ke semua tugas segera setelah dimulai. Langkah selanjutnya adalah memindahkan ekspresi await untuk hash browns dan telur ke bagian akhir dari metode, sebelum Anda menyajikan sarapan.

Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");

Task<Egg> eggsTask = FryEggsAsync(2);
Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
Task<Toast> toastTask = ToastBreadAsync(2);

Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready");
Juice oj = PourOJ();
Console.WriteLine("Oj is ready");

Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");

Console.WriteLine("Breakfast is ready!");

Anda sekarang memiliki sarapan yang disiapkan secara asinkron yang membutuhkan waktu sekitar 20 menit untuk disiapkan. Total waktu memasak berkurang karena beberapa tugas berjalan bersamaan.

Diagram yang menunjukkan instruksi untuk menyiapkan sarapan sebagai delapan tugas asinkron yang selesai dalam waktu sekitar 20 menit. Sayangnya, telur dan hash brown terbakar.

Pembaruan kode meningkatkan proses persiapan dengan mengurangi waktu memasak, tetapi mereka memperkenalkan regresi dengan membakar telur dan hash browns. Anda memulai semua tugas asinkron secara bersamaan. Anda hanya menunggu hasil dari setiap tugas saat membutuhkannya. Kode mungkin mirip dengan program dalam aplikasi web yang membuat permintaan ke layanan mikro yang berbeda dan kemudian menggabungkan hasilnya ke dalam satu halaman. Anda segera membuat semua permintaan, lalu menerapkan ekspresi await pada semua tugas tersebut dan menyusun halaman web.

Dukungan komposisi melalui tugas

Revisi kode sebelumnya membantu menyiapkan semuanya untuk sarapan pada saat yang sama, kecuali roti panggang. Proses membuat roti panggang adalah komposisi dari operasi asinkron (memanggang roti) dengan operasi sinkron (menyebarkan mentega dan selai pada roti panggang). Contoh ini menggambarkan konsep penting tentang pemrograman asinkron:

Penting

Komposisi operasi asinkron diikuti oleh pekerjaan sinkron adalah operasi asinkron. Dinyatakan dengan cara lain, jika ada bagian dari operasi yang asinkron, seluruh operasi adalah asinkron.

Dalam pembaruan sebelumnya, Anda mempelajari cara menggunakan objek Task atau Task<TResult> untuk menyimpan tugas yang sedang berjalan. Anda menunggu setiap tugas sebelum menggunakan hasilnya. Langkah selanjutnya adalah membuat metode yang mewakili kombinasi pekerjaan lain. Sebelum menyajikan sarapan, Anda perlu menyelesaikan tugas memanggang roti terlebih dahulu sebelum menyebarkan mentega dan selai.

Anda dapat mewakili pekerjaan ini dengan kode berikut:

static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
    var toast = await ToastBreadAsync(number);
    ApplyButter(toast);
    ApplyJam(toast);

    return toast;
}

Metode MakeToastWithButterAndJamAsync memiliki pengubah async dalam tanda tangannya yang memberi sinyal ke pengompilasi bahwa metode berisi ekspresi await dan berisi operasi asinkron. Metode ini mewakili tugas yang memanggang roti, kemudian menyebarkan mentega dan selai. Metode mengembalikan objek Task<TResult> yang mewakili komposisi tiga operasi.

Blok utama kode yang direvisi sekarang terlihat seperti ini:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");

    var eggsTask = FryEggsAsync(2);
    var hashBrownTask = FryHashBrownsAsync(3);
    var toastTask = MakeToastWithButterAndJamAsync(2);

    var eggs = await eggsTask;
    Console.WriteLine("eggs are ready");

    var hashBrown = await hashBrownTask;
    Console.WriteLine("hash browns are ready");

    var toast = await toastTask;
    Console.WriteLine("toast is ready");

    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");
    Console.WriteLine("Breakfast is ready!");
}

Perubahan kode ini menggambarkan teknik penting untuk bekerja dengan kode asinkron. Anda menyusun tugas dengan memisahkan operasi ke dalam metode baru yang mengembalikan tugas. Anda dapat memilih kapan harus menunggu pada tugas tersebut. Anda dapat memulai tugas lain secara bersamaan.

Mengatasi pengecualian asinkron

Hingga saat ini, kode Anda secara implisit mengasumsikan semua tugas berhasil diselesaikan. Metode asinkron melemparkan pengecualian, sama seperti rekan-rekan sinkron mereka. Tujuan untuk dukungan asinkron untuk pengecualian dan penanganan kesalahan sama dengan untuk dukungan asinkron secara umum. Praktik terbaik adalah menulis kode yang berbunyi seperti serangkaian pernyataan sinkron. Tugas menghasilkan pengecualian ketika tidak dapat diselesaikan dengan sukses. Kode klien dapat menangkap pengecualian tersebut saat ekspresi await diterapkan ke tugas yang dimulai.

Dalam contoh sarapan, misalkan pemanggang roti menangkap api saat memanggang roti. Anda dapat mensimulasikan masalah tersebut dengan memodifikasi metode ToastBreadAsync agar sesuai dengan kode berikut:

private static async Task<Toast> ToastBreadAsync(int slices)
{
    for (int slice = 0; slice < slices; slice++)
    {
        Console.WriteLine("Putting a slice of bread in the toaster");
    }
    Console.WriteLine("Start toasting...");
    await Task.Delay(2000);
    Console.WriteLine("Fire! Toast is ruined!");
    throw new InvalidOperationException("The toaster is on fire");
    await Task.Delay(1000);
    Console.WriteLine("Remove toast from toaster");

    return new Toast();
}

Nota

Saat mengkompilasi kode ini, Anda akan melihat peringatan tentang kode yang tidak dapat dijangkau. Kesalahan ini didesain. Setelah toaster terbakar, operasi tidak berjalan normal dan kode mengembalikan kesalahan.

Setelah Anda membuat perubahan kode, jalankan aplikasi dan periksa output:

Pouring coffee
Coffee is ready
Warming the egg pan...
putting 3 hash brown patties in the pan
Cooking first side of hash browns...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a hash brown patty
Flipping a hash brown patty
Flipping a hash brown patty
Cooking the second side of hash browns...
Cracking 2 eggs
Cooking the eggs ...
Put hash browns on plate
Put eggs on plate
Eggs are ready
Hash browns are ready
Unhandled exception. System.InvalidOperationException: The toaster is on fire
   at AsyncBreakfast.Program.ToastBreadAsync(Int32 slices) in Program.cs:line 65
   at AsyncBreakfast.Program.MakeToastWithButterAndJamAsync(Int32 number) in Program.cs:line 36
   at AsyncBreakfast.Program.Main(String[] args) in Program.cs:line 24
   at AsyncBreakfast.Program.<Main>(String[] args)

Perhatikan bahwa cukup banyak tugas selesai antara waktu ketika toaster terbakar dan sistem mendeteksi pengecualian. Ketika tugas yang berjalan secara asinkron melempar pengecualian, tugas tersebut rusak. Objek Task berfungsi untuk menyimpan pengecualian yang dilemparkan dalam properti Task.Exception. Tugas yang gagal melempar pengecualian ketika ekspresi await diterapkan ke tugas.

Ada dua mekanisme penting untuk dipahami tentang proses ini:

  • Bagaimana pengecualian disimpan dalam tugas yang mengalami kesalahan
  • Bagaimana pengecualian dibongkar dan dilempar ulang ketika kode menunggu (await) pada tugas yang mengalami kesalahan

Saat kode yang berjalan secara asinkron melemparkan pengecualian, pengecualian disimpan di objek Task. Properti Task.Exception adalah objek System.AggregateException karena lebih dari satu eksepsi mungkin terjadi selama pekerjaan asinkron. Pengecualian apa pun yang dilemparkan ditambahkan ke koleksi AggregateException.InnerExceptions. Jika properti Exception bernilai null, maka objek baru AggregateException akan dibuat dan pengecualian yang dilemparkan menjadi item pertama dalam koleksi.

Skenario paling umum untuk tugas yang salah adalah bahwa properti Exception berisi persis satu pengecualian. Ketika kode Anda menunggu pada tugas yang gagal, kode akan melempar ulang pengecualian AggregateException.InnerExceptions pertama dalam koleksi. Hasil ini adalah alasan mengapa output dari contoh menunjukkan objek System.InvalidOperationException daripada objek AggregateException. Mengekstrak pengecualian internal pertama membuat bekerja dengan metode asinkron semirip mungkin dengan bekerja menggunakan metode sinkron. Anda dapat memeriksa properti Exception dalam kode Anda saat skenario Anda mungkin menghasilkan beberapa pengecualian.

Saran

Praktik yang direkomendasikan adalah agar setiap pengecualian validasi argumen muncul secara sinkron dari metode yang mengembalikan tugas. Untuk informasi dan contoh selengkapnya, lihat Pengecualian dalam metode pengembalian tugas.

Sebelum Melanjutkan ke bagian berikutnya, komentari dua pernyataan berikut dalam metode ToastBreadAsync Anda. Anda tidak ingin memulai kebakaran lain:

Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");

Gunakan ekspresi 'await' pada tugas secara efisien

Anda dapat meningkatkan rangkaian ekspresi await di akhir kode sebelumnya dengan menggunakan metode kelas Task. Satu API adalah metode WhenAll, yang mengembalikan objek Task yang selesai ketika semua tugas dalam daftar argumennya selesai. Kode berikut menunjukkan metode ini:

await Task.WhenAll(eggsTask, hashBrownTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Hash browns are ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");

Opsi lain adalah menggunakan metode WhenAny, yang mengembalikan objek Task<Task> yang selesai ketika salah satu argumennya selesai. Anda bisa menunggu tugas yang dikembalikan karena Anda tahu tugas selesai. Kode berikut menunjukkan bagaimana Anda dapat menggunakan metode WhenAny untuk menunggu tugas pertama selesai lalu memproses hasilnya. Setelah Anda memproses hasil dari tugas yang selesai, Anda menghapus tugas yang selesai dari daftar tugas yang diteruskan ke metode WhenAny.

var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
while (breakfastTasks.Count > 0)
{
    Task finishedTask = await Task.WhenAny(breakfastTasks);
    if (finishedTask == eggsTask)
    {
        Console.WriteLine("Eggs are ready");
    }
    else if (finishedTask == hashBrownTask)
    {
        Console.WriteLine("Hash browns are ready");
    }
    else if (finishedTask == toastTask)
    {
        Console.WriteLine("Toast is ready");
    }
    await finishedTask;
    breakfastTasks.Remove(finishedTask);
}

Di dekat akhir cuplikan kode, perhatikan ekspresi await finishedTask;. Baris ini penting karena Task.WhenAny mengembalikan tugas - pembungkus Task<Task> yang berisi tugas yang telah selesai. Ketika Anda await Task.WhenAny, Anda menunggu tugas pembungkus selesai, dan hasilnya adalah tugas aktual yang selesai terlebih dahulu. Namun, untuk mengambil hasil tugas tersebut atau memastikan pengecualian apa pun dilemparkan dengan benar, Anda harus await menyelesaikan tugas itu sendiri (disimpan dalam finishedTask). Meskipun Anda tahu tugas telah selesai, menunggunya lagi memungkinkan Anda untuk mengakses hasilnya atau menangani pengecualian apa pun yang mungkin menyebabkan kesalahan.

Meninjau kode akhir

Berikut tampilan versi akhir kode:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace AsyncBreakfast
{
    // These classes are intentionally empty for the purpose of this example. They are simply marker classes for the purpose of demonstration, contain no properties, and serve no other purpose.
    internal class HashBrown { }
    internal class Coffee { }
    internal class Egg { }
    internal class Juice { }
    internal class Toast { }

    class Program
    {
        static async Task Main(string[] args)
        {
            Coffee cup = PourCoffee();
            Console.WriteLine("coffee is ready");

            var eggsTask = FryEggsAsync(2);
            var hashBrownTask = FryHashBrownsAsync(3);
            var toastTask = MakeToastWithButterAndJamAsync(2);

            var breakfastTasks = new List<Task> { eggsTask, hashBrownTask, toastTask };
            while (breakfastTasks.Count > 0)
            {
                Task finishedTask = await Task.WhenAny(breakfastTasks);
                if (finishedTask == eggsTask)
                {
                    Console.WriteLine("eggs are ready");
                }
                else if (finishedTask == hashBrownTask)
                {
                    Console.WriteLine("hash browns are ready");
                }
                else if (finishedTask == toastTask)
                {
                    Console.WriteLine("toast is ready");
                }
                await finishedTask;
                breakfastTasks.Remove(finishedTask);
            }

            Juice oj = PourOJ();
            Console.WriteLine("oj is ready");
            Console.WriteLine("Breakfast is ready!");
        }

        static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
        {
            var toast = await ToastBreadAsync(number);
            ApplyButter(toast);
            ApplyJam(toast);

            return toast;
        }

        private static Juice PourOJ()
        {
            Console.WriteLine("Pouring orange juice");
            return new Juice();
        }

        private static void ApplyJam(Toast toast) =>
            Console.WriteLine("Putting jam on the toast");

        private static void ApplyButter(Toast toast) =>
            Console.WriteLine("Putting butter on the toast");

        private static async Task<Toast> ToastBreadAsync(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("Putting a slice of bread in the toaster");
            }
            Console.WriteLine("Start toasting...");
            await Task.Delay(3000);
            Console.WriteLine("Remove toast from toaster");

            return new Toast();
        }

        private static async Task<HashBrown> FryHashBrownsAsync(int patties)
        {
            Console.WriteLine($"putting {patties} hash brown patties in the pan");
            Console.WriteLine("cooking first side of hash browns...");
            await Task.Delay(3000);
            for (int patty = 0; patty < patties; patty++)
            {
                Console.WriteLine("flipping a hash brown patty");
            }
            Console.WriteLine("cooking the second side of hash browns...");
            await Task.Delay(3000);
            Console.WriteLine("Put hash browns on plate");

            return new HashBrown();
        }

        private static async Task<Egg> FryEggsAsync(int howMany)
        {
            Console.WriteLine("Warming the egg pan...");
            await Task.Delay(3000);
            Console.WriteLine($"cracking {howMany} eggs");
            Console.WriteLine("cooking the eggs ...");
            await Task.Delay(3000);
            Console.WriteLine("Put eggs on plate");

            return new Egg();
        }

        private static Coffee PourCoffee()
        {
            Console.WriteLine("Pouring coffee");
            return new Coffee();
        }
    }
}

Kode menyelesaikan tugas sarapan asinkron dalam waktu sekitar 15 menit. Total waktu berkurang karena beberapa tugas berjalan bersamaan. Kode secara bersamaan memantau beberapa tugas dan mengambil tindakan hanya sesuai kebutuhan.

Diagram yang menunjukkan instruksi untuk menyiapkan sarapan sebagai enam tugas asinkron yang selesai dalam waktu sekitar 15 menit, dan kode memantau kemungkinan gangguan.

Kode akhir tidak sinkron. Ini lebih akurat mencerminkan bagaimana seseorang dapat memasak sarapan. Bandingkan kode akhir dengan sampel kode pertama dalam artikel. Tindakan inti masih jelas dengan membaca kode. Anda dapat membaca kode akhir dengan cara yang sama seperti Anda membaca daftar instruksi untuk membuat sarapan, seperti yang ditunjukkan di awal artikel. Fitur bahasa untuk kata kunci async dan await menyediakan terjemahan yang dibuat setiap orang untuk mengikuti instruksi tertulis: Mulai tugas yang Anda bisa dan jangan blokir saat menunggu tugas selesai.

Asinkron/tunggu vs LanjutkanWith

Kata kunci async dan await memberikan penyederhanaan sintaks daripada penggunaan Task.ContinueWith secara langsung. Meskipun async/await dan ContinueWith memiliki semantik serupa untuk menangani operasi asinkron, pengompilasi tidak selalu menerjemahkan await ekspresi langsung ke dalam ContinueWith panggilan metode. Sebaliknya, kompilator menghasilkan kode komputer status yang dioptimalkan yang menyediakan perilaku logis yang sama. Transformasi ini memberikan manfaat keterbacaan dan pemeliharaan yang signifikan, terutama ketika menautkan beberapa operasi asinkron.

Pertimbangkan skenario di mana Anda perlu melakukan beberapa operasi asinkron berurutan. Berikut adalah bagaimana tampilan logika yang sama saat diimplementasikan dengan ContinueWith dibandingkan dengan async/await.

Menggunakan ContinueWith

Dengan ContinueWith, setiap langkah dalam urutan operasi asinkron memerlukan kelanjutan berlapis:

// Using ContinueWith - demonstrates the complexity when chaining operations
static Task MakeBreakfastWithContinueWith()
{
    return StartCookingEggsAsync()
        .ContinueWith(eggsTask =>
        {
            var eggs = eggsTask.Result;
            Console.WriteLine("Eggs ready, starting bacon...");
            return StartCookingBaconAsync();
        })
        .Unwrap()
        .ContinueWith(baconTask =>
        {
            var bacon = baconTask.Result;
            Console.WriteLine("Bacon ready, starting toast...");
            return StartToastingBreadAsync();
        })
        .Unwrap()
        .ContinueWith(toastTask =>
        {
            var toast = toastTask.Result;
            Console.WriteLine("Toast ready, applying butter...");
            return ApplyButterAsync(toast);
        })
        .Unwrap()
        .ContinueWith(butteredToastTask =>
        {
            var butteredToast = butteredToastTask.Result;
            Console.WriteLine("Butter applied, applying jam...");
            return ApplyJamAsync(butteredToast);
        })
        .Unwrap()
        .ContinueWith(finalToastTask =>
        {
            var finalToast = finalToastTask.Result;
            Console.WriteLine("Breakfast completed with ContinueWith!");
        });
}

Menggunakan asinkron/menunggu

Urutan operasi yang sama menggunakan async/await terasa jauh lebih alami.

// Using async/await - much cleaner and easier to read
static async Task MakeBreakfastWithAsyncAwait()
{
    var eggs = await StartCookingEggsAsync();
    Console.WriteLine("Eggs ready, starting bacon...");
    
    var bacon = await StartCookingBaconAsync();
    Console.WriteLine("Bacon ready, starting toast...");
    
    var toast = await StartToastingBreadAsync();
    Console.WriteLine("Toast ready, applying butter...");
    
    var butteredToast = await ApplyButterAsync(toast);
    Console.WriteLine("Butter applied, applying jam...");
    
    var finalToast = await ApplyJamAsync(butteredToast);
    Console.WriteLine("Breakfast completed with async/await!");
}

Mengapa asinkron/menunggu lebih disukai

Pendekatan ini async/await menawarkan beberapa keuntungan:

  • Keterbacaan: Kode membaca seperti kode sinkron, sehingga lebih mudah untuk memahami alur operasi.
  • Ketahanan: Menambahkan atau menghapus langkah-langkah dalam urutan memerlukan perubahan kode minimal.
  • Penanganan kesalahan: Penanganan pengecualian dengan try/catch blok berfungsi secara alami, sedangkan ContinueWith memerlukan penanganan tugas yang rusak dengan hati-hati.
  • Debugging: Tumpukan panggilan dan pengalaman debugger jauh lebih baik dengan async/await.
  • Performa: Optimalisasi kompilator untuk async/await lebih canggih daripada rangkaian manual.ContinueWith

Manfaatnya menjadi lebih jelas ketika jumlah operasi berantai meningkat. Meskipun satu kelanjutan mungkin dapat dikelola dengan ContinueWith, urutan 3-4 atau lebih operasi asinkron dengan cepat menjadi sulit dibaca dan dipertahankan. Pola ini, yang dikenal sebagai "monadic do-notation" dalam pemrograman fungsional, memungkinkan Anda menyusun beberapa operasi asinkron secara berurutan dan dapat dibaca.

Langkah berikutnya