Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
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
backgroundTaskadalah hal yang memungkinkan pelacakan. Tugas yang tidak dapat Anda rujuk adalah tugas yang tidak dapat Anda kosongkan atau amati. -
Daftar dengan pelacak —
tracker.Trackmelampirkan 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 matikan —
tracker.DrainAsyncmenunggu 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 senyap —
InvalidOperationExceptiondari 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
Taskdan 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.
- Berhenti menerima pekerjaan baru.
- Pembatalan sinyal.
- Tunggu tugas terlacak dengan batas waktu terikat.
- 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.