Skenario pemrograman asinkron

Jika Anda memiliki kebutuhan terikat I/O (seperti meminta data dari jaringan, mengakses database, atau membaca dan menulis ke sistem file), Anda sebaiknya menggunakan pemrograman asinkron. Anda juga dapat memiliki kode terikat CPU, seperti melakukan perhitungan yang mahal, yang juga merupakan skenario yang baik untuk menulis kode asinkron.

C# memiliki model pemrograman asinkron tingkat bahasa, yang memungkinkan penulisan kode asinkron dengan mudah tanpa harus beralih panggilan balik atau menyesuaikan dengan pustaka yang mendukung asinkron. Model ini mengikuti apa yang dikenal sebagai Pola Asinkron Berbasis Tugas (TAP).

Gambaran umum model asinkron

Inti dari pemrograman asinkron adalah objek Task dan Task<T>, yang memodelkan operasi asinkron. Objek tersebut didukung oleh kata kunci async dan await. Model ini cukup sederhana dalam banyak kasus:

  • Untuk kode terikat I/O, Anda menunggu operasi yang mengembalikan Task atau Task<T> di dalam metode async.
  • Untuk kode terikat CPU, Anda menunggu operasi yang dimulai pada utas latar belakang dengan metode Task.Run.

Kata kunci await adalah tempat keajaiban terjadi. Ini menghasilkan kontrol kepada pemanggil metode yang melakukan await, dan pada akhirnya memungkinkan antarmuka pengguna menjadi responsif atau layanan menjadi elastis. Meskipun ada cara lain untuk mendekati kode asinkron selain async dan await, artikel ini berfokus pada konstruksi tingkat bahasa.

Catatan

Dalam beberapa contoh System.Net.Http.HttpClient kelas berikut digunakan untuk mengunduh beberapa data dari layanan web. Objek s_httpClient yang digunakan dalam contoh ini adalah bidang Program statis kelas (silakan periksa contoh lengkapnya):

private static readonly HttpClient s_httpClient = new();

Contoh terikat I/O: Pengunduhan data dari layanan web

Anda mungkin perlu mengunduh beberapa data dari layanan web saat tombol ditekan, tetapi tidak ingin memblokir utas antarmuka pengguna. Ini dapat dicapai seperti berikut ini:

s_downloadButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI as the request
    // from the web service is happening.
    //
    // The UI thread is now free to perform other work.
    var stringData = await s_httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

Kode mengekspresikan niat (mengunduh data secara asinkron) tanpa terjebak dalam interaksi dengan objek Task.

Contoh terikat CPU: Melakukan penghitungan untuk game

Katakanlah Anda sedang menulis game seluler di mana menekan tombol dapat menimbulkan kerusakan pada banyak musuh di layar. Melakukan kalkulasi kerusakan bisa mahal, dan melakukannya pada utas antarmuka pengguna akan membuat game tampak terjeda saat penghitungan dilakukan!

Cara terbaik untuk menangani hal ini adalah dengan memulai utas latar belakang, yang melakukan pekerjaan menggunakan Task.Run, dan menunggu hasilnya menggunakan await. Hal ini memungkinkan antarmuka pengguna untuk merasa lancar saat pekerjaan sedang dilakukan.

static DamageResult CalculateDamageDone()
{
    return new DamageResult()
    {
        // Code omitted:
        //
        // Does an expensive calculation and returns
        // the result of that calculation.
    };
}

s_calculateButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI while CalculateDamageDone()
    // performs its work. The UI thread is free to perform other work.
    var damageResult = await Task.Run(() => CalculateDamageDone());
    DisplayDamage(damageResult);
};

Kode ini dengan jelas mengungkapkan niat dari peristiwa klik tombol, tidak memerlukan pengelolaan utas latar belakang secara manual, dan melakukannya dengan cara yang tidak memblokir.

Apa yang sebenarnya terjadi

Di sisi C#, kompilator mengubah kode Anda menjadi mesin status yang melacak hal-hal seperti menghasilkan eksekusi ketika await tercapai dan melanjutkan eksekusi ketika pekerjaan latar belakang telah selesai.

Untuk yang cenderung teoritis, ini adalah implementasi dari Promise Model asinkron.

Bagian-bagian kunci untuk dipahami

  • Kode asinkron dapat digunakan untuk kode terikat I/O dan terikat CPU, tetapi berbeda untuk setiap skenario.
  • Kode asinkron menggunakan Task<T> dan Task, yang merupakan konstruksi yang digunakan untuk memodelkan pekerjaan yang dilakukan di latar belakang.
  • Kata kunci async mengubah metode menjadi metode asinkron, yang memungkinkan Anda menggunakan kata kunci await dalam isinya.
  • Ketika kata kunci await diterapkan, kata kunci menangguhkan metode panggilan dan menghasilkan kontrol kembali ke pemanggilnya hingga tugas yang ditunggu selesai.
  • await hanya dapat digunakan di dalam metode asinkron.

Mengenali pekerjaan yang terikat CPU dan terikat I/O

Dua contoh pertama dari panduan ini menunjukkan bagaimana Anda dapat menggunakan async dan await untuk pekerjaan terikat I/O dan terikat CPU. Ini adalah kunci di mana Anda dapat mengidentifikasi ketika pekerjaan yang perlu Anda lakukan adalah terikat I/O atau terikat CPU karena dapat sangat memengaruhi performa kode Anda dan berpotensi menyebabkan penyalahgunaan konstruksi tertentu.

Berikut adalah dua pertanyaan yang harus Anda ajukan sebelum menulis kode apa pun:

  1. Apakah kode Anda akan "menunggu" sesuatu, seperti data dari database?

    Jika jawaban Anda adalah "ya", maka pekerjaan Anda terikat I/O.

  2. Apakah kode Anda akan melakukan komputasi yang mahal?

    Jika Anda menjawab "ya", maka pekerjaan Anda terikat CPU.

Jika pekerjaan yang Anda miliki terikat I/O, gunakan async dan awaittanpaTask.Run. Anda tidak boleh menggunakan Task Parallel Library.

Jika pekerjaan yang Anda miliki terikat CPU dan Anda peduli dengan responsivitas, gunakan async dan await, tetapi keluarkan pekerjaan di utas lain denganTask.Run. Jika pekerjaan sesuai untuk konkurensi dan paralelisme, pertimbangkan juga untuk menggunakan Task Parallel Library.

Selain itu, Anda harus selalu mengukur eksekusi kode Anda. Misalnya, Anda mungkin menemukan diri Anda dalam situasi di mana pekerjaan terikat CPU Anda tidak cukup mahal dibandingkan dengan overhead pengalihan konteks saat membuat beberapa utas. Setiap pilihan memiliki barter, dan Anda harus memilih barter yang tepat untuk situasi Anda.

Contoh lainnya

Contoh berikut menunjukkan berbagai cara Anda dapat menulis kode asinkron di C#. Mereka mencakup beberapa skenario berbeda yang mungkin Anda temui.

Mengekstrak data dari jaringan

Cuplikan ini mengunduh HTML dari URL yang diberikan dan menghitung berapa kali string ".NET" terjadi dalam HTML. Ini menggunakan ASP.NET untuk menentukan metode pengontrol API Web, yang melakukan tugas ini dan mengembalikan angka.

Catatan

Jika Anda berencana melakukan penguraian HTML dalam kode produksi, jangan gunakan ekspresi reguler. Gunakan pustaka penguraian sebagai gantinya.

[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
    // Suspends GetDotNetCount() to allow the caller (the web server)
    // to accept another request, rather than blocking on this one.
    var html = await s_httpClient.GetStringAsync(URL);
    return Regex.Matches(html, @"\.NET").Count;
}

Berikut adalah skenario yang sama yang ditulis untuk Aplikasi Universal Windows, yang melakukan tugas yang sama saat Tombol ditekan:

private readonly HttpClient _httpClient = new HttpClient();

private async void OnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)
{
    // Capture the task handle here so we can await the background task later.
    var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");

    // Any other work on the UI thread can be done here, such as enabling a Progress Bar.
    // This is important to do here, before the "await" call, so that the user
    // sees the progress bar before execution of this method is yielded.
    NetworkProgressBar.IsEnabled = true;
    NetworkProgressBar.Visibility = Visibility.Visible;

    // The await operator suspends OnSeeTheDotNetsButtonClick(), returning control to its caller.
    // This is what allows the app to be responsive and not block the UI thread.
    var html = await getDotNetFoundationHtmlTask;
    int count = Regex.Matches(html, @"\.NET").Count;

    DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";

    NetworkProgressBar.IsEnabled = false;
    NetworkProgressBar.Visibility = Visibility.Collapsed;
}

Tunggu hingga beberapa tugas selesai

Anda mungkin menemukan diri Anda dalam situasi di mana Anda perlu mengambil beberapa bagian data secara bersamaan. API Task berisi dua metode, Task.WhenAll dan Task.WhenAny, yang memungkinkan Anda menulis kode asinkron yang menunggu tanpa pemblokiran pada beberapa pekerjaan latar belakang.

Contoh ini menunjukkan bagaimana Anda dapat mengambil data User untuk satu set userId.

private static async Task<User> GetUserAsync(int userId)
{
    // Code omitted:
    //
    // Given a user Id {userId}, retrieves a User object corresponding
    // to the entry in the database with {userId} as its Id.

    return await Task.FromResult(new User() { id = userId });
}

private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = new List<Task<User>>();
    foreach (int userId in userIds)
    {
        getUserTasks.Add(GetUserAsync(userId));
    }

    return await Task.WhenAll(getUserTasks);
}

Berikut adalah cara lain untuk menulis ini dengan lebih ringkas menggunakan LINQ:

private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
    var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
    return await Task.WhenAll(getUserTasks);
}

Meskipun kodenya lebih sedikit, berhati-hatilah saat mencampur LINQ dengan kode asinkron. Karena LINQ menggunakan eksekusi yang ditangguhkan (malas), panggilan asinkron tidak akan segera terjadi seperti yang terjadi dalam perulangan foreach kecuali Anda memaksa urutan yang dihasilkan untuk melakukan beralih dengan panggilan ke .ToList() atau .ToArray(). Contoh di atas menggunakan Enumerable.ToArray untuk melakukan kueri dengan bersemangat dan menyimpan hasilnya dalam array. Itu memaksa kode id => GetUserAsync(id) untuk menjalankan dan memulai tugas.

Info dan saran penting

Dengan pemrograman asinkron, ada beberapa detail yang perlu diingat yang dapat mencegah perilaku tak terduga.

  • asyncmetode harus memiliki kata kunci awaitdalam isi atau tidak akan pernah menghasilkan!

    Penting untuk mengingat ini. Jika await tidak digunakan dalam isi metode async, kompiler C# mengeluarkan peringatan, tetapi kode mengompilasi dan berjalan seolah-olah itu adalah metode normal. Ini sangat tidak efisien, karena mesin status yang dihasilkan oleh kompiler C# untuk metode asinkron tidak mencapai apa pun.

  • Tambahkan "Asinkron" sebagai akhiran dari setiap nama metode asinkron yang Anda tulis.

    Ini adalah konvensi yang digunakan dalam .NET untuk lebih mudah membedakan metode sinkron dan asinkron. Metode tertentu yang tidak secara eksplisit dipanggil oleh kode Anda (seperti penanganan aktivitas atau metode pengontrol web) tidak selalu berlaku. Karena mereka tidak secara eksplisit dipanggil oleh kode Anda, menjadi eksplisit tentang penamaan mereka tidak begitu penting.

  • async voidhanya boleh digunakan untuk penanganan aktivitas.

    async void adalah satu-satunya cara untuk memungkinkan penanganan aktivitas asinkron berfungsi karena peristiwa tidak memiliki jenis pengembalian (sehingga tidak dapat menggunakan Task dan Task<T>). Penggunaan lain async void tidak mengikuti model TAP dan dapat menjadi tantangan untuk digunakan, seperti:

    • Pengecualian yang dilemparkan dalam metode async void tidak dapat ditangkap di luar metode itu.
    • Metode async void sulit diuji.
    • Metode async void dapat menyebabkan efek samping yang buruk jika pemanggil tidak mengharapkannya menjadi asinkron.
  • Hati-hati saat menggunakan lambda asinkron dalam ekspresi LINQ

    Ekspresi Lambda dalam LINQ menggunakan eksekusi yang ditangguhkan, yang berarti kode dapat berakhir dieksekusi pada saat Anda tidak mengharapkannya. Pengenalan tugas pemblokiran ke dalamnya dapat dengan mudah mengakibatkan kebuntuan jika tidak ditulis dengan benar. Selain itu, bersarangnya kode asinkron seperti ini juga dapat mempersulit pertimbangan eksekusi kode. Asinkron dan LINQ sangat kuat, tetapi harus digunakan bersama-sama dengan hati-hati dan sejelas mungkin.

  • Menulis kode yang menunggu Tugas dengan cara yang tidak memblokir

    Memblokir utas saat ini sebagai sarana untuk menunggu Task selesai dapat mengakibatkan kebuntuan dan utas konteks yang diblokir dan dapat memerlukan penanganan kesalahan yang lebih kompleks. Tabel berikut ini menyediakan panduan tentang cara menangani menunggu tugas dengan cara yang tidak memblokir:

    Gunakan ini... Alih-alih ini... Ketika ingin melakukan ini...
    await Task.Wait atau Task.Result Mengambil hasil proses di latar belakang
    await Task.WhenAny Task.WaitAny Menunggu tugas apa pun selesai
    await Task.WhenAll Task.WaitAll Menunggu semua tugas selesai
    await Task.Delay Thread.Sleep Menunggu jangka waktu tertentu
  • Pertimbangkan untuk menggunakanValueTaskjika memungkinkan

    Mengembalikan objek Task dari metode asinkron dapat menyebabkan kemacetan performa di jalur tertentu. Task adalah jenis referensi, jadi menggunakannya berarti mengalokasikan objek. Dalam kasus di mana metode yang dinyatakan dengan pengubah async mengembalikan hasil yang di-cache atau diselesaikan secara sinkron, alokasi tambahan dapat menjadi biaya waktu yang signifikan dalam bagian kode yang kritis performa. Ini bisa menjadi mahal jika alokasi tersebut terjadi dalam perulangan yang ketat. Untuk informasi selengkapnya, lihat jenis pengembalian asinkron umum.

  • Pertimbangkan untuk menggunakanConfigureAwait(false)

    Pertanyaan umumnya adalah, "kapan saya harus menggunakan metode Task.ConfigureAwait(Boolean) ini?". Metode ini memungkinkan instans Task untuk mengonfigurasi awaiter-nya. Ini adalah pertimbangan penting dan pengaturan yang salah berpotensi memiliki implikasi performa dan bahkan kebuntuan. Untuk informasi selengkapnya tentang ConfigureAwait, lihat ConfigureAwait FAQ.

  • Menulis kode yang kurang stateful

    Jangan bergantung pada status objek global atau eksekusi metode tertentu. Sebaliknya, hanya bergantung pada nilai metode yang dikembalikan. Mengapa?

    • Kode akan lebih mudah untuk dipikirkan.
    • Kode akan lebih mudah diuji.
    • Mencampur kode asinkron dan sinkron jauh lebih sederhana.
    • Kondisi race biasanya dapat dihindari sama sekali.
    • Bergantung pada nilai pengembalian membuat koordinasi kode asinkron menjadi sederhana.
    • (Bonus) ini bekerja sangat baik dengan injeksi dependensi.

Tujuan yang disarankan adalah untuk mencapai Transparansi Referensial yang lengkap atau hampir lengkap dalam kode Anda. Melakukannya akan menghasilkan basis kode yang dapat diprediksi, dapat diuji, dan dapat dipertahankan.

Contoh lengkap

Kode berikut adalah teks lengkap dari file Program.cs sebagai contoh.

using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;

class Button
{
    public Func<object, object, Task>? Clicked
    {
        get;
        internal set;
    }
}

class DamageResult
{
    public int Damage
    {
        get { return 0; }
    }
}

class User
{
    public bool isEnabled
    {
        get;
        set;
    }

    public int id
    {
        get;
        set;
    }
}

public class Program
{
    private static readonly Button s_downloadButton = new();
    private static readonly Button s_calculateButton = new();

    private static readonly HttpClient s_httpClient = new();

    private static readonly IEnumerable<string> s_urlList = new string[]
    {
            "https://learn.microsoft.com",
            "https://learn.microsoft.com/aspnet/core",
            "https://learn.microsoft.com/azure",
            "https://learn.microsoft.com/azure/devops",
            "https://learn.microsoft.com/dotnet",
            "https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio",
            "https://learn.microsoft.com/education",
            "https://learn.microsoft.com/shows/net-core-101/what-is-net",
            "https://learn.microsoft.com/enterprise-mobility-security",
            "https://learn.microsoft.com/gaming",
            "https://learn.microsoft.com/graph",
            "https://learn.microsoft.com/microsoft-365",
            "https://learn.microsoft.com/office",
            "https://learn.microsoft.com/powershell",
            "https://learn.microsoft.com/sql",
            "https://learn.microsoft.com/surface",
            "https://dotnetfoundation.org",
            "https://learn.microsoft.com/visualstudio",
            "https://learn.microsoft.com/windows",
            "https://learn.microsoft.com/maui"
    };

    private static void Calculate()
    {
        // <PerformGameCalculation>
        static DamageResult CalculateDamageDone()
        {
            return new DamageResult()
            {
                // Code omitted:
                //
                // Does an expensive calculation and returns
                // the result of that calculation.
            };
        }

        s_calculateButton.Clicked += async (o, e) =>
        {
            // This line will yield control to the UI while CalculateDamageDone()
            // performs its work. The UI thread is free to perform other work.
            var damageResult = await Task.Run(() => CalculateDamageDone());
            DisplayDamage(damageResult);
        };
        // </PerformGameCalculation>
    }

    private static void DisplayDamage(DamageResult damage)
    {
        Console.WriteLine(damage.Damage);
    }

    private static void Download(string URL)
    {
        // <UnblockingDownload>
        s_downloadButton.Clicked += async (o, e) =>
        {
            // This line will yield control to the UI as the request
            // from the web service is happening.
            //
            // The UI thread is now free to perform other work.
            var stringData = await s_httpClient.GetStringAsync(URL);
            DoSomethingWithData(stringData);
        };
        // </UnblockingDownload>
    }

    private static void DoSomethingWithData(object stringData)
    {
        Console.WriteLine("Displaying data: ", stringData);
    }

    // <GetUsersForDataset>
    private static async Task<User> GetUserAsync(int userId)
    {
        // Code omitted:
        //
        // Given a user Id {userId}, retrieves a User object corresponding
        // to the entry in the database with {userId} as its Id.

        return await Task.FromResult(new User() { id = userId });
    }

    private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
    {
        var getUserTasks = new List<Task<User>>();
        foreach (int userId in userIds)
        {
            getUserTasks.Add(GetUserAsync(userId));
        }

        return await Task.WhenAll(getUserTasks);
    }
    // </GetUsersForDataset>

    // <GetUsersForDatasetByLINQ>
    private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
    {
        var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
        return await Task.WhenAll(getUserTasks);
    }
    // </GetUsersForDatasetByLINQ>

    // <ExtractDataFromNetwork>
    [HttpGet, Route("DotNetCount")]
    static public async Task<int> GetDotNetCount(string URL)
    {
        // Suspends GetDotNetCount() to allow the caller (the web server)
        // to accept another request, rather than blocking on this one.
        var html = await s_httpClient.GetStringAsync(URL);
        return Regex.Matches(html, @"\.NET").Count;
    }
    // </ExtractDataFromNetwork>

    static async Task Main()
    {
        Console.WriteLine("Application started.");

        Console.WriteLine("Counting '.NET' phrase in websites...");
        int total = 0;
        foreach (string url in s_urlList)
        {
            var result = await GetDotNetCount(url);
            Console.WriteLine($"{url}: {result}");
            total += result;
        }
        Console.WriteLine("Total: " + total);

        Console.WriteLine("Retrieving User objects with list of IDs...");
        IEnumerable<int> ids = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
        var users = await GetUsersAsync(ids);
        foreach (User? user in users)
        {
            Console.WriteLine($"{user.id}: isEnabled={user.isEnabled}");
        }

        Console.WriteLine("Application ending.");
    }
}

// Example output:
//
// Application started.
// Counting '.NET' phrase in websites...
// https://learn.microsoft.com: 0
// https://learn.microsoft.com/aspnet/core: 57
// https://learn.microsoft.com/azure: 1
// https://learn.microsoft.com/azure/devops: 2
// https://learn.microsoft.com/dotnet: 83
// https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio: 31
// https://learn.microsoft.com/education: 0
// https://learn.microsoft.com/shows/net-core-101/what-is-net: 42
// https://learn.microsoft.com/enterprise-mobility-security: 0
// https://learn.microsoft.com/gaming: 0
// https://learn.microsoft.com/graph: 0
// https://learn.microsoft.com/microsoft-365: 0
// https://learn.microsoft.com/office: 0
// https://learn.microsoft.com/powershell: 0
// https://learn.microsoft.com/sql: 0
// https://learn.microsoft.com/surface: 0
// https://dotnetfoundation.org: 16
// https://learn.microsoft.com/visualstudio: 0
// https://learn.microsoft.com/windows: 0
// https://learn.microsoft.com/maui: 6
// Total: 238
// Retrieving User objects with list of IDs...
// 1: isEnabled= False
// 2: isEnabled= False
// 3: isEnabled= False
// 4: isEnabled= False
// 5: isEnabled= False
// 6: isEnabled= False
// 7: isEnabled= False
// 8: isEnabled= False
// 9: isEnabled= False
// 0: isEnabled= False
// Application ending.

Sumber daya lainnya