Contoh async / await yang menyebabkan kebuntuan

97

Saya menemukan beberapa praktik terbaik untuk pemrograman asinkron menggunakan c # 's async/ awaitkeywords (Saya baru mengenal c # 5.0).

Salah satu saran yang diberikan adalah sebagai berikut:

Stabilitas: Ketahui konteks sinkronisasi Anda

... Beberapa konteks sinkronisasi non-reentrant dan single-threaded. Ini berarti hanya satu unit pekerjaan yang dapat dijalankan dalam konteks pada waktu tertentu. Contohnya adalah utas Windows UI atau konteks permintaan ASP.NET. Dalam konteks sinkronisasi utas tunggal ini, Anda mudah menemui jalan buntu. Jika Anda mengeluarkan tugas dari konteks utas tunggal, lalu menunggu tugas itu dalam konteks, kode tunggu Anda mungkin memblokir tugas latar belakang.

public ActionResult ActionAsync()
{
    // DEADLOCK: this blocks on the async task
    var data = GetDataAsync().Result;

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

Jika saya mencoba membedahnya sendiri, utas utama muncul ke utas baru MyWebService.GetDataAsync();, tetapi karena utas utama menunggu di sana, utas utama menunggu hasilnya masuk GetDataAsync().Result. Sementara itu, katakanlah data sudah siap. Mengapa utas utama tidak melanjutkan logika kelanjutannya dan mengembalikan hasil string dari GetDataAsync()?

Adakah yang bisa menjelaskan kepada saya mengapa ada kebuntuan dalam contoh di atas? Saya benar-benar tidak tahu apa masalahnya ...

Dror Weiss
sumber
Apakah Anda benar-benar yakin bahwa GetDataAsync menyelesaikan tugasnya? Atau macet hanya menyebabkan kunci dan bukan kebuntuan?
Andrey
Ini adalah contoh yang diberikan. Menurut pemahaman saya, itu harus menyelesaikan hal-hal itu dan memiliki hasil semacam siap ...
Dror Weiss
4
Mengapa Anda bahkan menunggu tugas? Anda harus menunggu karena pada dasarnya Anda kehilangan semua manfaat model asinkron.
Toni Petrina
Untuk menambah poin @ ToniPetrina, bahkan tanpa masalah kebuntuan, var data = GetDataAsync().Result;adalah baris kode yang tidak boleh dilakukan dalam konteks yang tidak boleh Anda blokir (permintaan UI atau ASP.NET). Bahkan jika tidak mengalami kebuntuan, itu memblokir utas dalam waktu yang tidak pasti. Jadi pada dasarnya ini adalah contoh yang buruk. [Anda harus keluar dari utas UI sebelum menjalankan kode seperti itu, atau gunakan di awaitsana juga, seperti yang disarankan Toni.]
ToolmakerSteve

Jawaban:

82

Lihat contoh ini , Stephen punya jawaban yang jelas untuk Anda:

Jadi inilah yang terjadi, dimulai dengan metode tingkat atas ( Button1_Clickuntuk UI / MyController.Getuntuk ASP.NET):

  1. Panggilan metode tingkat atas GetJsonAsync(dalam konteks UI / ASP.NET).

  2. GetJsonAsyncmemulai permintaan REST dengan memanggil HttpClient.GetStringAsync(masih dalam konteks).

  3. GetStringAsyncmengembalikan belum selesai Task, menunjukkan permintaan REST belum selesai.

  4. GetJsonAsyncmenunggu Taskdikembalikan oleh GetStringAsync. Konteksnya ditangkap dan akan digunakan untuk melanjutkan menjalankan GetJsonAsyncmetode nanti. GetJsonAsyncmengembalikan yang belum selesai Task, yang menunjukkan bahwa GetJsonAsyncmetode tersebut belum selesai.

  5. Metode tingkat atas secara sinkron memblokir pada yang Taskdikembalikan oleh GetJsonAsync. Ini memblokir utas konteks.

  6. ... Akhirnya, permintaan REST akan selesai. Ini melengkapi Taskyang dikembalikan oleh GetStringAsync.

  7. Kelanjutan untuk GetJsonAsyncsekarang siap dijalankan, dan menunggu konteks tersedia sehingga dapat dijalankan dalam konteks.

  8. Kebuntuan . Metode tingkat atas memblokir utas konteks, menunggu GetJsonAsynchingga selesai, dan GetJsonAsyncmenunggu konteks bebas sehingga dapat diselesaikan. Untuk contoh UI, "konteks" adalah konteks UI; untuk contoh ASP.NET, "konteks" adalah konteks permintaan ASP.NET. Jenis kebuntuan ini dapat disebabkan oleh salah satu "konteks".

Tautan lain yang harus Anda baca: Tunggu, dan UI, dan kebuntuan! Astaga!

cuongle
sumber
23
  • Fakta 1: GetDataAsync().Result;akan berjalan saat tugas dikembalikan oleh GetDataAsync()selesai, sementara itu memblokir thread UI
  • Fakta 2: Kelanjutan await ( return result.ToString()) diantrekan ke thread UI untuk dieksekusi
  • Fakta 3: Tugas yang dikembalikan oleh GetDataAsync()akan selesai saat antrian lanjutannya dijalankan
  • Fakta 4: Antrian lanjutan tidak pernah berjalan, karena UI thread diblokir (Fakta 1)

Jalan buntu!

Kebuntuan dapat dipecahkan dengan menyediakan alternatif untuk menghindari Fakta 1 atau Fakta 2.

  • Hindari 1,4. Alih-alih memblokir UI thread, gunakan var data = await GetDataAsync()yang memungkinkan thread UI tetap berjalan
  • Hindari 2,3. Mengantre kelanjutan menunggu ke utas berbeda yang tidak diblokir, mis. Penggunaan var data = Task.Run(GetDataAsync).Result, yang akan memposting kelanjutan ke konteks sinkronisasi utas threadpool. Ini memungkinkan tugas yang dikembalikan oleh GetDataAsync()selesai.

Ini dijelaskan dengan sangat baik dalam artikel oleh Stephen Toub , sekitar setengah jalan di mana dia menggunakan contoh DelayAsync().

Phillip Ngan
sumber
Mengenai,, var data = Task.Run(GetDataAsync).Resultitu baru bagi saya. Saya selalu berpikir bagian luar .Resultakan tersedia segera setelah menunggu pertama GetDataAsyncdipukul, jadi dataakan selalu tersedia default. Menarik.
nawfal
19

Saya baru saja mengotak-atik masalah ini lagi dalam proyek ASP.NET MVC. Saat Anda ingin memanggil asyncmetode dari a PartialView, Anda tidak diizinkan membuat PartialView async. Anda akan mendapatkan pengecualian jika melakukannya.

Anda dapat menggunakan solusi sederhana berikut dalam skenario di mana Anda ingin memanggil asyncmetode dari metode sinkronisasi:

  1. Sebelum panggilan, hapus SynchronizationContext
  2. Lakukan panggilan, tidak akan ada lagi kebuntuan disini, tunggu sampai selesai
  3. Pulihkan SynchronizationContext

Contoh:

public ActionResult DisplayUserInfo(string userName)
{
    // trick to prevent deadlocks of calling async method 
    // and waiting for on a sync UI thread.
    var syncContext = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(null);

    //  this is the async call, wait for the result (!)
    var model = _asyncService.GetUserInfo(Username).Result;

    // restore the context
    SynchronizationContext.SetSynchronizationContext(syncContext);

    return PartialView("_UserInfo", model);
}
Herre Kuijpers
sumber
3

Poin utama lainnya adalah Anda tidak boleh memblokir Tasks, dan menggunakan asinkron sepenuhnya untuk mencegah kebuntuan. Maka itu akan menjadi pemblokiran tidak sinkron semua asynchronous.

public async Task<ActionResult> ActionAsync()
{

    var data = await GetDataAsync();

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}
marvelTracker
sumber
6
Bagaimana jika saya ingin utas utama (UI) diblokir hingga tugas selesai? Atau di aplikasi Konsol misalnya? Katakanlah saya ingin menggunakan HttpClient, yang hanya mendukung async ... Bagaimana cara menggunakannya secara sinkron tanpa risiko kebuntuan ? Ini harus memungkinkan. Jika WebClient dapat digunakan dengan cara itu (karena memiliki metode sinkronisasi) dan berfungsi dengan sempurna, mengapa itu tidak dapat dilakukan dengan HttpClient juga?
Dexter
Lihat jawaban dari Philip Ngan di atas (saya tahu ini telah diposting setelah komentar ini): Antrean kelanjutan menunggu ke utas berbeda yang tidak diblokir, misalnya gunakan var data = Task.Run (GetDataAsync) .Result
Jeroen
@Dexter - re " Bagaimana jika saya ingin utas utama (UI) diblokir sampai tugas selesai? " - apakah Anda benar-benar ingin utas UI diblokir, yang berarti pengguna tidak dapat melakukan apa pun, bahkan tidak dapat membatalkan - atau apakah Anda tidak ingin melanjutkan metode Anda saat ini? "await" atau "Task.ContinueWith" menangani kasus terakhir.
ToolmakerSteve
@ToolmakerSteve tentu saja saya tidak ingin melanjutkan metode ini. Tapi saya tidak bisa menggunakan await karena saya juga tidak bisa menggunakan async sepenuhnya - HttpClient dipanggil di main , yang tentu saja tidak bisa async. Dan kemudian saya menyebutkan melakukan semua ini di aplikasi Konsol - dalam hal ini saya menginginkan yang pertama - saya tidak ingin aplikasi saya bahkan menjadi multi-utas. Blokir semuanya .
Dexter
-1

Solusi yang saya temukan adalah menggunakan Joinmetode ekstensi pada tugas sebelum meminta hasilnya.

Kodenya terlihat seperti ini:

public ActionResult ActionAsync()
{
  var task = GetDataAsync();
  task.Join();
  var data = task.Result;

  return View(data);
}

Dimana metode bergabungnya adalah:

public static class TaskExtensions
{
    public static void Join(this Task task)
    {
        var currentDispatcher = Dispatcher.CurrentDispatcher;
        while (!task.IsCompleted)
        {
            // Make the dispatcher allow this thread to work on other things
            currentDispatcher.Invoke(delegate { }, DispatcherPriority.SystemIdle);
        }
    }
}

Saya tidak cukup masuk ke domain untuk melihat kekurangan dari solusi ini (jika ada)

Orace
sumber