Bug umum pada penggunaan async/await

Asinkron/menunggu menyederhanakan pemrograman asinkron, tetapi kesalahan tertentu muncul berulang kali. Artikel ini menjelaskan lima bug paling umum dalam kode asinkron dan menunjukkan kepada Anda cara memperbaiki masing-masing bug.

Metode asinkron berjalan secara sinkron

Menambahkan kata kunci async ke metode tidak membuat metode berjalan di utas latar belakang. Ini memberi tahu pengkompilasi untuk memungkinkan await di dalam isi metode dan untuk membungkus nilai pengembalian dalam Task. Saat Anda memanggil metode asynchronous, metode tersebut berlangsung sinkron hingga mencapai awaitable pertama yang belum selesai. Jika metode tidak mengandung ekspresi await, atau jika setiap awaitable yang ditunggu sudah dipenuhi, metode selesai sepenuhnya pada utas panggilan.

public static class SyncExecutionExample
{
    public static Task<int> ComputeAsync()
    {
        // No await in this method — it runs entirely synchronously.
        return Task.FromResult(42);
    }
}
Public Module SyncExecutionExample
    Public Function ComputeAsync() As Task(Of Integer)
        ' No Await in this method — it runs entirely synchronously.
        Return Task.FromResult(42)
    End Function
End Module

Di sini metode segera mengembalikan tugas yang diselesaikan karena tidak pernah menghasilkan. Pengkompilasi memancarkan peringatan ketika metode asinkron tidak memiliki await ekspresi.

Jika tujuan Anda adalah untuk memindahkan pekerjaan yang bergantung pada CPU ke pool utas, gunakan Run alih-alih async:

public static class OffloadExample
{
    public static int ComputeIntensive()
    {
        int sum = 0;
        for (int i = 0; i < 1_000; i++)
            sum += i;
        return sum;
    }

    public static Task<int> ComputeOnThreadPoolAsync()
    {
        return Task.Run(() => ComputeIntensive());
    }
}
Public Module OffloadExample
    Public Function ComputeIntensive() As Integer
        Dim sum As Integer = 0
        For i As Integer = 0 To 999
            sum += i
        Next
        Return sum
    End Function

    Public Function ComputeOnThreadPoolAsync() As Task(Of Integer)
        Return Task.Run(Function() ComputeIntensive())
    End Function
End Module

Untuk panduan selengkapnya tentang kapan menggunakan Task.Run, lihat Pembungkus asinkron untuk metode sinkron.

Tidak dapat menunggu metode void asinkron

Saat Anda mengonversi metode void yang mengembalikan sinkron ke asinkron, ubah jenis pengembalian menjadi Task. Jika Anda membiarkan jenis pengembalian sebagai void, metode menjadi "asinkron batal," yang tidak dapat Anda tunggu:

public static class AsyncVoidExample
{
    // BAD: async void — can't be awaited.
    public static async void DoWorkBadAsync()
    {
        await Task.Delay(100);
    }

    // GOOD: async Task — callers can await this.
    public static async Task DoWorkGoodAsync()
    {
        await Task.Delay(100);
    }
}
Public Module AsyncVoidExample
    ' BAD: Async Sub — can't be awaited.
    Public Async Sub DoWorkBadAsync()
        Await Task.Delay(100)
    End Sub

    ' GOOD: Async Function returning Task — callers can await this.
    Public Async Function DoWorkGoodAsync() As Task
        Await Task.Delay(100)
    End Function
End Module

Metode kekosongan asinkron melayani tujuan tertentu: penanganan aktivitas tingkat atas dalam kerangka kerja UI. Di luar penanganan aktivitas, selalu kembalikan Task atau Task<T> dari metode asinkron. Metode void asinkron memiliki kelemahan berikut:

  • Pengecualian tidak teramati. Pengecualian yang dilemparkan dalam metode void asinkron menyebar ke SynchronizationContext yang aktif ketika metode dimulai. Penelepon tidak dapat menangkap pengecualian ini.
  • Penelepon tidak dapat melacak penyelesaian. Tanpa Task, tidak ada mekanisme untuk mengetahui kapan operasi selesai.
  • Pengujian sulit. Anda tidak dapat menunggu metode dalam pengujian untuk memverifikasi perilakunya.

Kebuntuan akibat pemblokiran pada kode asinkron

Bug ini adalah penyebab paling umum dari kode asinkron yang "tidak pernah selesai." Ini terjadi ketika Anda secara sinkron memblokir (memanggil Wait, Task<TResult>.Result, atau GetAwaiter.GetResult) pada sebuah utas yang memiliki utas tunggal SynchronizationContext.

Urutan yang menyebabkan kebuntuan:

  1. Kode pada utas UI (atau utas permintaan ASP.NET di ASP.NET yang lebih lama) memanggil metode asinkron dan blok pada tugas yang dikembalikan.
  2. Metode asinkron menunggu tugas yang tidak lengkap tanpa menggunakan ConfigureAwait(false).
  3. Ketika tugas yang ditunggu selesai, proses kelanjutan berusaha untuk mengirim kembali ke SynchronizationContext yang asli.
  4. Utas konteks tersebut diblokir menunggu tugas selesai—kondisi deadlock.
public static class DeadlockExample
{
    public static async Task<string> GetDataAsync()
    {
        // Without ConfigureAwait(false), this continuation
        // posts back to the original SynchronizationContext.
        await Task.Delay(100);
        return "data";
    }

    public static void CallerThatDeadlocks()
    {
        // On a single-threaded SynchronizationContext (e.g. UI thread),
        // the following line deadlocks because the continuation needs
        // the same thread that .Result is blocking.
        string result = GetDataAsync().Result;
    }
}
Public Module DeadlockExample
    Public Async Function GetDataAsync() As Task(Of String)
        ' Without ConfigureAwait(False), this continuation
        ' posts back to the original SynchronizationContext.
        Await Task.Delay(100)
        Return "data"
    End Function

    Public Sub CallerThatDeadlocks()
        ' On a single-threaded SynchronizationContext (e.g. UI thread),
        ' the following line deadlocks because the continuation needs
        ' the same thread that .Result is blocking.
        Dim result As String = GetDataAsync().Result
    End Sub
End Module

Cara menghindari kebuntuan

Gunakan satu atau beberapa strategi ini:

  • Jangan blokir. Gunakan await alih-alih .Result atau .Wait():

    public static class DeadlockFix1
    {
        public static async Task CallerFixedAsync()
        {
            // Use await instead of .Result
            string result = await DeadlockExample.GetDataAsync();
            Console.WriteLine(result);
        }
    }
    
    Public Module DeadlockFix1
        Public Async Function CallerFixedAsync() As Task
            ' Use Await instead of .Result
            Dim result As String = Await DeadlockExample.GetDataAsync()
            Console.WriteLine(result)
        End Function
    End Module
    
  • Gunakan ConfigureAwait(false) dalam kode pustaka. Saat metode pustaka Anda tidak perlu melanjutkan konteks pemanggil, tentukan ConfigureAwait(false) pada setiap await:

    public static class DeadlockFix2
    {
        public static async Task<string> GetDataSafeAsync()
        {
            await Task.Delay(100).ConfigureAwait(false);
            return "data";
        }
    }
    
    Public Module DeadlockFix2
        Public Async Function GetDataSafeAsync() As Task(Of String)
            Await Task.Delay(100).ConfigureAwait(False)
            Return "data"
        End Function
    End Module
    

    Menggunakan ConfigureAwait(false) menginstruksikan runtime untuk tidak memarshall kelanjutan kembali ke yang asli SynchronizationContext. Pendekatan ini melindungi penelepon yang memblokir, dan meningkatkan performa dengan menghindari lompatan utas yang tidak perlu.

Peringatan

Kemacetan konstruktor statis. CLR memegang kunci saat menjalankan konstruktor statis (cctors). Jika konstruktor statis menghentikan suatu tugas, dan jika kelanjutan tugas tersebut perlu menjalankan kode dalam jenis yang sama (atau jenis yang terlibat dalam rantai konstruksi), kelanjutan tidak dapat berlanjut karena kunci cctor sedang dipegang. Hindari sepenuhnya memblokir panggilan di dalam konstruktor statis.

Tugas<pembongkaran tugas>

Ketika Anda meneruskan lambda asinkron ke metode seperti StartNew, objek yang dikembalikan adalah Task<Task> (atau Task<Task<TResult>>), bukan sederhana Task. Tugas luar selesai segera setelah lambda asinkron mencapai titik pengembalian pertamanya await. Tidak menunggu tugas internal selesai.

public static class TaskTaskBugExample
{
    public static async Task DemoAsync()
    {
        var sw = Stopwatch.StartNew();
        // StartNew returns Task<Task>, not Task.
        // The outer task completes immediately when the lambda yields.
        await Task.Factory.StartNew(async () =>
        {
            await Task.Delay(1000);
        });
        // Elapsed shows ~0 seconds, not ~1 second.
        Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s");
    }
}
Public Module TaskTaskBugExample
    Public Async Function DemoAsync() As Task
        Dim sw = Stopwatch.StartNew()
        ' StartNew returns Task(Of Task), not Task.
        ' The outer task completes immediately when the lambda yields.
        Await Task.Factory.StartNew(Async Function()
                                        Await Task.Delay(1000)
                                    End Function)
        ' Elapsed shows ~0 seconds, not ~1 second.
        Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s")
    End Function
End Module

Perbaiki masalah ini dengan salah satu dari tiga cara:

  • Gunakan Run sebagai gantinya. Task.Run secara otomatis membongkar Task<Task>:

    public static class TaskTaskFix1
    {
        public static async Task DemoAsync()
        {
            var sw = Stopwatch.StartNew();
            await Task.Run(async () =>
            {
                await Task.Delay(1000);
            });
            Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s");
        }
    }
    
    Public Module TaskTaskFix1
        Public Async Function DemoAsync() As Task
            Dim sw = Stopwatch.StartNew()
            Await Task.Run(Async Function()
                               Await Task.Delay(1000)
                           End Function)
            Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s")
        End Function
    End Module
    
  • Panggil Unwrap pada hasilnya:

    public static class TaskTaskFix2
    {
        public static async Task DemoAsync()
        {
            var sw = Stopwatch.StartNew();
            await Task.Factory.StartNew(async () =>
            {
                await Task.Delay(1000);
            }).Unwrap();
            Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s");
        }
    }
    
    Public Module TaskTaskFix2
        Public Async Function DemoAsync() As Task
            Dim sw = Stopwatch.StartNew()
            Await Task.Factory.StartNew(Async Function()
                                            Await Task.Delay(1000)
                                        End Function).Unwrap()
            Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s")
        End Function
    End Module
    
  • Tunggu dua kali (pertama tugas luar, lalu bagian dalam):

    public static class TaskTaskFix3
    {
        public static async Task DemoAsync()
        {
            var sw = Stopwatch.StartNew();
            Task<Task> outerTask = Task.Factory.StartNew(async () =>
            {
                await Task.Delay(1000);
            });
            Task innerTask = await outerTask;
            await innerTask;
            Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s");
        }
    }
    
    Public Module TaskTaskFix3
        Public Async Function DemoAsync() As Task
            Dim sw = Stopwatch.StartNew()
            Dim outerTask As Task(Of Task) = Task.Factory.StartNew(Async Function()
                                                                       Await Task.Delay(1000)
                                                                   End Function)
            Dim innerTask As Task = Await outerTask
            Await innerTask
            Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s")
        End Function
    End Module
    

Tidak ada yang menunggu pada panggilan pengembalian tugas

Jika Anda memanggil metode yang mengembalikan tugas dalam metode async tanpa menunggunya, maka metode tersebut akan memulai operasi asinkron tanpa menunggu penyelesaiannya. Pengkompilasi memperingatkan Anda tentang kasus ini dengan CS4014 di C# dan BC42358 di Visual Basic:

public static class MissingAwaitExample
{
    // BAD: Task.Delay is started but never awaited.
    public static async Task PauseOneSecondBuggyAsync()
    {
        Task.Delay(1000); // CS4014 warning
    }

    // GOOD: await the task.
    public static async Task PauseOneSecondAsync()
    {
        await Task.Delay(1000);
    }
}
Public Module MissingAwaitExample
    ' BAD: Task.Delay is started but never awaited.
    Public Async Function PauseOneSecondBuggyAsync() As Task
        Task.Delay(1000) ' Warning BC42358
    End Function

    ' GOOD: Await the task.
    Public Async Function PauseOneSecondAsync() As Task
        Await Task.Delay(1000)
    End Function
End Module

Menyimpan hasil dalam variabel menekan peringatan tetapi tidak memperbaiki bug yang mendasar. Selalu await tugas kecuali Anda sengaja menginginkan perilaku kirim dan lupakan.

Baca juga