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.
Ketika pustaka hanya mengekspos API asinkron, konsumen terkadang membungkusnya dalam panggilan sinkron untuk memenuhi antarmuka atau kontrak yang sinkron. Pola "sync-over-async" ini bisa tampak mudah, tetapi merupakan sumber umum kebuntuan dan masalah kinerja.
Pola pembungkusan dasar
Pembungkus sinkron di sekitar metode Pola Asinkron Berbasis Tugas (TAP) mengakses properti tugas Result , yang memblokir utas panggilan:
public class TapWrapper
{
public static int Foo(Func<Task<int>> fooAsync)
{
return fooAsync().Result;
}
}
Public Module TapWrapper
Public Function Foo(fooAsync As Func(Of Task(Of Integer))) As Integer
Return fooAsync().Result
End Function
End Module
Pendekatan ini terlihat sederhana, tetapi dapat menyebabkan masalah serius tergantung pada lingkungan tempatnya berjalan.
Kebuntuan dengan konteks berutas tunggal
Skenario paling berbahaya terjadi ketika Anda memanggil pembungkus sinkron dari utas yang memiliki SynchronizationContext utas tunggal. Skenario ini biasanya merupakan utas UI di aplikasi WPF, Formulir Windows, atau MAUI.
public static class DeadlockExample
{
private static void Delay(int milliseconds)
{
DelayAsync(milliseconds).Wait();
}
private static async Task DelayAsync(int milliseconds)
{
await Task.Delay(milliseconds);
}
}
Public Module DeadlockExample
Private Sub Delay(milliseconds As Integer)
DelayAsync(milliseconds).Wait()
End Sub
Private Async Function DelayAsync(milliseconds As Integer) As Task
Await Task.Delay(milliseconds)
End Function
End Module
Inilah yang terjadi langkah demi langkah:
- Utas
Delaymemanggil UI, yang kemudian memanggilDelayAsync(milliseconds).Wait(). -
DelayAsyncberjalan secara sinkron sampai mencapaiawait Task.Delay(milliseconds). - Karena penundaan belum selesai,
awaitmenangkap SynchronizationContext saat ini dan menghentikan sementara.DelayAsyncmengembalikan Task ke pemanggilnya. - Utas UI diblokir di
.Wait(), menunggu hingga tugas tersebut selesai. - Ketika penundaan selesai, kelanjutan perlu berjalan pada aslinya
SynchronizationContextyang merupakan utas UI. - Utas UI tidak dapat memproses kelanjutan karena diblokir di
.Wait(). - Kehadangan.
Penting
Keberhasilan atau kegagalan kode sync-over-async tergantung pada lingkungan di mana ia dijalankan. Kode yang berfungsi di aplikasi konsol mungkin mengalami kebuntuan pada utas UI atau di ASP.NET (di .NET Framework). Dependensi lingkungan ini adalah alasan inti untuk menghindari mengekspos pembungkus sinkron.
Kehabisan kolam utas
Kebuntuan tidak terbatas pada utas UI. Jika metode asinkron tergantung pada kumpulan utas untuk menyelesaikan pekerjaannya, misalnya, dengan mengantre langkah pemrosesan akhir, memblokir banyak rangkaian kumpulan dengan pembungkus sinkron dapat membuat kumpulan kelaparan:
public static class ThreadPoolDeadlockExample
{
public static int Foo(Func<Task<int>> fooAsync)
{
return fooAsync().Result;
}
public static async Task DemonstrateDeadlockRiskAsync()
{
var tasks = Enumerable.Range(0, 25)
.Select(_ => Task.Run(() => Foo(() => SomeIOOperationAsync())));
await Task.WhenAll(tasks);
}
private static async Task<int> SomeIOOperationAsync()
{
await Task.Delay(100);
return 42;
}
}
Dalam skenario ini:
- Banyak utas kumpulan utas memanggil
Foo, yang memblokir di.Result. - Setiap operasi asinkron menyelesaikan I/O-nya dan memerlukan thread dari kumpulan thread untuk menjalankan callback penyelesaian.
- Karena panggilan yang diblokir menempati utas pekerja yang tersedia, penyelesaian mungkin harus menunggu lama sampai sebuah utas menjadi tersedia.
- .NET modern dapat menambahkan lebih banyak utas kumpulan utas dari waktu ke waktu, tetapi aplikasi masih dapat mengalami kelaparan kumpulan utas yang parah, throughput buruk, penundaan panjang, atau hang yang jelas.
Pola ini mempengaruhi HttpWebRequest.GetResponse dalam .NET Framework 1.x, di mana metode sinkron diimplementasikan sebagai pembungkus untuk metode asinkron BeginGetResponse/EndGetResponse.
Pedoman: Hindari mengekspos pembungkus sinkron
Jangan mengekspos metode sinkron yang membungkus implementasi asinkron. Sebaliknya, tinggalkan keputusan apakah akan memblokir kepada konsumen. Pengguna mengetahui lingkungan threading mereka dan dapat membuat pilihan yang terinformasi.
Jika Anda merasa perlu memanggil metode asinkron secara sinkron, pertimbangkan terlebih dahulu apakah Anda dapat merestrukturisasi kode menjadi "asinkron sepanjang jalan ke bawah." Pemfaktoran ulang sering kali merupakan solusi jangka panjang yang lebih baik.
Strategi mitigasi saat sync-over-async tidak dapat dihindari
Terkadang sync-over-async benar-benar tidak dapat ditolak. Misalnya, itu tidak dapat ditolak ketika Anda menerapkan antarmuka yang memerlukan metode sinkron, dan satu-satunya implementasi yang tersedia adalah asinkron. Dalam kasus tersebut, terapkan strategi berikut untuk mengurangi risiko.
Gunakan ConfigureAwait(false) dalam implementasi asinkron
Jika Anda mengontrol metode asinkron, gunakan Task.ConfigureAwait dengan false pada setiap await untuk mencegah kelanjutan dari marshaling kembali ke SynchronizationContext yang asli.
public static class ConfigureAwaitMitigation
{
public static async Task<int> LibraryMethodAsync()
{
await Task.Delay(100).ConfigureAwait(false);
return 42;
}
public static int Sync()
{
return LibraryMethodAsync().GetAwaiter().GetResult();
}
}
Public Module ConfigureAwaitMitigation
Public Async Function LibraryMethodAsync() As Task(Of Integer)
Await Task.Delay(100).ConfigureAwait(False)
Return 42
End Function
Public Function Sync() As Integer
Return LibraryMethodAsync().Result
End Function
End Module
Sebagai penulis pustaka, gunakan ConfigureAwait(false) pada semua await kecuali jika kode Anda secara khusus perlu dilanjutkan dalam konteks yang sedang aktif. Menggunakan ConfigureAwait(false) adalah praktik terbaik untuk performa dan membantu mencegah kebuntuan saat konsumen memblokir.
Offload ke kolam thread
Jika Anda tidak mengontrol implementasi asinkron (dan mungkin tidak menggunakan ConfigureAwait(false)), pindahkan panggilan ke kumpulan utas. Kumpulan utas tidak memiliki SynchronizationContext, sehingga menunggu tidak akan mencoba untuk mengalokasikan kembali kontrol ke utas yang diblokir.
public int Sync()
{
return Task.Run(() => Library.FooAsync()).Result;
}
Public Function Sync() As Integer
Return Task.Run(Function() Library.FooAsync()).Result
End Function
Menguji di beberapa lingkungan
Jika Anda harus mengirim pembungkus sinkron, uji dari:
- Utas UI (WPF, Formulir Windows).
- Kolam utas sedang dalam kondisi beban.
- Kumpulan utas dengan jumlah utas maksimum rendah.
- Aplikasi konsol.
Perilaku yang berfungsi di satu lingkungan mungkin mengalami deadlock di lingkungan lain.