Menjaga metode asinkron tetap berfungsi

Pekerjaan fire-and-forget mudah dimulai dan mudah hilang. Jika Anda memulai operasi asinkron dan menghilangkan yang dikembalikan Task, Anda kehilangan visibilitas ke penyelesaian, pembatalan, dan kegagalan.

Sebagian besar bug seumur hidup dalam kode asinkron adalah bug kepemilikan, bukan bug kompilator. Mesin status async dan Task terjaga sementara pekerjaan masih dapat diakses melalui kelanjutan proses. Masalah terjadi saat aplikasi Anda tidak lagi melacak pekerjaan tersebut.

Mengapa konsep "fire-and-forget" menyebabkan bug seumur hidup

Saat Anda memulai pekerjaan latar belakang tanpa melacaknya, Anda membuat tiga risiko:

  • Operasi dapat gagal, dan tidak ada yang mengamati pengecualian.
  • Proses atau host dapat dimatikan sebelum operasi selesai.
  • Operasi ini dapat bertahan lebih lama dari objek atau cakupan yang dimaksudkan untuk mengontrolnya.

Gunakan fire-and-forget hanya ketika pekerjaan benar-benar opsional dan kegagalan dapat diterima.

Melacak pekerjaan latar belakang secara eksplisit

Sampel ini mendefinisikan BackgroundTaskTracker, kelas pembantu kustom yang menyimpan kamus tugas dalam penerbangan yang aman utas. Ketika Anda memanggil Track, ia mendaftarkan ContinueWith kelanjutan pada tugas yang menghapus tugas dari kamus saat tugas selesai dan mencatat kegagalan jika ada. Saat Anda memanggil DrainAsync, Task.WhenAll akan dipanggil pada setiap tugas yang masih ada dalam kamus dan kemudian mengembalikan tugas yang dihasilkan.

public sealed class BackgroundTaskTracker
{
    private readonly ConcurrentDictionary<int, Task> _inFlight = new();

    public void Track(Task operationTask, string name)
    {
        int id = operationTask.Id;
        _inFlight[id] = operationTask;

        _ = operationTask.ContinueWith(completedTask =>
        {
            _inFlight.TryRemove(id, out _);

            if (completedTask.IsFaulted)
            {
                Console.WriteLine($"{name} failed: {completedTask.Exception?.GetBaseException().Message}");
            }
        }, TaskScheduler.Default);
    }

    public Task DrainAsync()
    {
        Task[] snapshot = _inFlight.Values.ToArray();
        return snapshot.Length == 0 ? Task.CompletedTask : Task.WhenAll(snapshot);
    }
}
Public NotInheritable Class BackgroundTaskTracker
    Private ReadOnly _inFlight As New ConcurrentDictionary(Of Integer, Task)()

    Public Sub Track(operationTask As Task, name As String)
        Dim id As Integer = operationTask.Id
        _inFlight(id) = operationTask

        Dim continuationTask As Task = operationTask.ContinueWith(Sub(completedTask)
                                                                      Dim removedTask As Task = Nothing
                                                                      _inFlight.TryRemove(id, removedTask)

                                                                      If completedTask.IsFaulted Then
                                                                          Console.WriteLine($"{name} failed: {completedTask.Exception.GetBaseException().Message}")
                                                                      End If
                                                                  End Sub,
                                                                  TaskScheduler.Default)
    End Sub

    Public Function DrainAsync() As Task
        Dim snapshot As Task() = _inFlight.Values.ToArray()

        If snapshot.Length = 0 Then
            Return Task.CompletedTask
        End If

        Return Task.WhenAll(snapshot)
    End Function
End Class

Contoh berikut menggunakan BackgroundTaskTracker untuk memulai, memantau, dan menghentikan operasi latar belakang.

public static class FireAndForgetFix
{
    public static async Task RunAsync(BackgroundTaskTracker tracker)
    {
        Task backgroundTask = Task.Run(async () =>
        {
            await Task.Delay(100);
            throw new InvalidOperationException("Background operation failed.");
        });

        tracker.Track(backgroundTask, "Cache refresh");

        try
        {
            await tracker.DrainAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Drain observed failure: {ex.GetBaseException().Message}");
        }
    }
}
Public Module FireAndForgetFix
    Public Async Function RunAsync(tracker As BackgroundTaskTracker) As Task
        Dim backgroundTask As Task = Task.Run(Async Function()
                                                  Await Task.Delay(100)
                                                  Throw New InvalidOperationException("Background operation failed.")
                                              End Function)

        tracker.Track(backgroundTask, "Cache refresh")

        Try
            Await tracker.DrainAsync()
        Catch ex As Exception
            Console.WriteLine($"Drain observed failure: {ex.GetBaseException().Message}")
        End Try
    End Function
End Module

Anda mungkin bertanya: jika DrainAsync hanya menunggu satu tugas yang Anda mulai, mengapa tidak await backgroundTask secara langsung dan melewati pelacak sepenuhnya? Untuk satu tugas dalam satu metode, Anda bisa. Pelacak menjadi berharga ketika tugas dimulai dari berbagai tempat di seluruh masa pakai komponen. Setiap pemanggil menyerahkan tugasnya ke pelacak bersama, dan satu panggilan DrainAsync saat penutupan menunggu semua tanpa mengetahui berapa banyak yang telah dimulai atau siapa yang memulainya. Pelacak juga memberlakukan kebijakan pelaksanaan pengecualian yang konsisten: setiap tugas terdaftar mendapatkan proses pencatatan kesalahan yang sama, sehingga tidak ada pengecualian yang dapat terlewat tanpa disadari, terlepas dari jalur program mana yang memulai pekerjaan.

Tiga komponen utama pola yang dilacak adalah:

  • Tetapkan tugas ke variabel — menyimpan referensi ke backgroundTask adalah hal yang memungkinkan pelacakan. Tugas yang tidak dapat Anda rujuk adalah tugas yang tidak dapat Anda kosongkan atau amati.
  • Daftar dengan pelacaktracker.Track melampirkan kelanjutan pencatatan kegagalan dan menambahkan tugas ke set yang sedang berjalan. Setiap pengecualian yang dilemparkan oleh pekerjaan latar belakang muncul melalui lanjutan itu alih-alih menghilang tanpa jejak.
  • Menguras saat matikantracker.DrainAsync menunggu semuanya masih berjalan. Panggil sebelum komponen atau proses Anda keluar untuk menjamin tidak ada pekerjaan dalam penerbangan yang ditinggalkan pada pertengahan penerbangan.

Dampak dari fire-and-forget yang tidak terlacak

Jika Anda membuang hasil yang dikembalikan Task alih-alih melacaknya, Anda menciptakan kegagalan tanpa disadari:

public static class FireAndForgetPitfall
{
    public static async Task RunAsync()
    {
        _ = Task.Run(async () =>
        {
            await Task.Delay(100);
            throw new InvalidOperationException("Background operation failed.");
        });

        await Task.Delay(150);
        Console.WriteLine("Caller finished without observing background completion.");
    }
}
Public Module FireAndForgetPitfall
    Public Async Function RunAsync() As Task
        Dim discardedTask As Task = Task.Run(Async Function()
                                                 Await Task.Delay(100)
                                                 Throw New InvalidOperationException("Background operation failed.")
                                             End Function)

        Await Task.Delay(150)
        Console.WriteLine("Caller finished without observing background completion.")
    End Function
End Module

Tiga masalah timbul akibat menghentikan tugas:

  • Pengecualian senyapInvalidOperationException dari operasi latar belakang tidak pernah diamati. Runtime mengarahkannya ke UnobservedTaskException pada finalisasi, yang tidak dapat diprediksi dan terlalu terlambat untuk bisa ditangani dengan baik.
  • Tidak ada koordinasi matikan — pemanggil berlanjut dan keluar tanpa menunggu operasi selesai. Pada proses berumur pendek atau host dengan batas waktu penutupan, pekerjaan latar belakang dibatalkan atau hilang sepenuhnya.
  • Tidak ada visibilitas — tanpa referensi ke tugas, Anda tidak dapat menentukan apakah operasi berhasil, gagal, atau masih berjalan.

Fire-and-forget yang tidak terlacak hanya dapat diterima ketika ketiga kondisi berikut dipenuhi: pekerjaan benar-benar opsional, kegagalan aman untuk diabaikan, dan operasi selesai dengan baik sebelum masa operasi yang diharapkan berakhir. Mencatat ping telemetri non-kritis adalah salah satu contoh di mana semua kondisi ini dapat terpenuhi.

Menjaga kepemilikan tetap eksplisit

Gunakan salah satu model kepemilikan ini:

  • Kembalikan Task dan minta penelepon untuk menunggunya.
  • Lacak tugas latar belakang di layanan yang ditentukan pemilik.
  • Gunakan abstraksi latar belakang yang dikelola host sehingga host memiliki masa pakai.

Jika pekerjaan harus dilanjutkan setelah pemanggil kembali, transfer kepemilikan secara eksplisit. Misalnya, serahkan tugas ke pelacak yang mencatat kesalahan dan terlibat dalam penonaktifan.

Menampilkan pengecualian dari tugas latar belakang

Tugas yang ditinggalkan dapat gagal tanpa terlihat sampai terjadi finalisasi dan penanganan pengecualian yang tidak teramati. Waktu tersebut tidak deterministik dan terlambat untuk penanganan permintaan atau alur kerja normal.

Lampirkan logika pengamatan saat Anda menjadwalkan pekerjaan latar belakang. Setidaknya, kegagalan dalam pencatatan log dalam kelanjutan. Lebih suka pelacak terpusat sehingga setiap operasi antrean mendapatkan kebijakan yang sama.

Untuk detail propagasi pengecualian, lihat Penanganan pengecualian tugas.

Koordinasi pembatalan dan penutupan

Mengikat pekerjaan latar belakang ke token pembatalan yang mewakili masa pakai aplikasi atau operasi. Selama pemadaman.

  1. Berhenti menerima pekerjaan baru.
  2. Pembatalan sinyal.
  3. Tunggu tugas terlacak dengan batas waktu terikat.
  4. Catat operasi yang tidak lengkap.

Alur ini menjaga proses pematian sistem tetap dapat diprediksi dan mencegah penulisan parsial atau operasi terisolasi.

Dapatkah GC mengumpulkan metode asinkron sebelum selesai?

Runtime menjaga mesin status asinkron tetap hidup sementara kelanjutan masih mereferensikannya. Anda biasanya tidak kehilangan operasi asinkron dalam penerbangan untuk pengumpulan sampah komputer status itu sendiri.

Anda masih dapat kehilangan kebenaran jika Anda kehilangan kepemilikan tugas yang dikembalikan, membuang sumber daya yang diperlukan lebih awal, atau membiarkan proses berakhir sebelum selesai. Fokus pada kepemilikan tugas dan penutupan terkoordinasi.

Baca juga