Mengapa tindakan asinkron ini hang?


102

Saya memiliki aplikasi multi-tier .Net 4.5 yang memanggil metode menggunakan kata kunci baru C # asyncdan awaityang baru saja hang dan saya tidak dapat melihat alasannya.

Di bagian bawah saya memiliki metode async yang memperluas utilitas database kami OurDBConn(pada dasarnya pembungkus untuk objek DBConnectiondan yang mendasarinya DBCommand):

public static async Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    T result = await Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });

    return result;
}

Lalu saya memiliki metode asinkron tingkat menengah yang memanggil ini untuk mendapatkan beberapa total yang berjalan lambat:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var result = await this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));

    return result;
}

Akhirnya saya memiliki metode UI (tindakan MVC) yang berjalan secara sinkron:

Task<ResultClass> asyncTask = midLevelClass.GetTotalAsync(...);

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

Masalahnya adalah itu tergantung pada baris terakhir itu selamanya. Itu melakukan hal yang sama jika saya menelepon asyncTask.Wait(). Jika saya menjalankan metode SQL lambat secara langsung dibutuhkan sekitar 4 detik.

Perilaku yang saya harapkan adalah ketika sampai asyncTask.Result, jika belum selesai harus menunggu sampai selesai, dan setelah itu akan mengembalikan hasilnya.

Jika saya melangkah melalui debugger, pernyataan SQL selesai dan fungsi lambda selesai, tetapi return result;baris GetTotalAsynctidak pernah tercapai.

Tahu apa yang saya lakukan salah?

Adakah saran di mana saya perlu menyelidiki untuk memperbaikinya?

Mungkinkah ini menjadi jalan buntu di suatu tempat, dan jika demikian apakah ada cara langsung untuk menemukannya?

Jawaban:


150

Ya, itu jalan buntu. Dan kesalahan umum dengan TPL, jadi jangan merasa buruk.

Saat Anda menulis await foo, runtime, secara default, menjadwalkan kelanjutan fungsi pada SynchronizationContext yang sama dengan tempat metode dimulai. Dalam bahasa Inggris, katakanlah Anda menelepon Anda ExecuteAsyncdari utas UI. Kueri Anda berjalan di utas threadpool (karena Anda memanggil Task.Run), tetapi Anda kemudian menunggu hasilnya. Ini berarti runtime akan menjadwalkan return result;baris " " Anda untuk dijalankan kembali di thread UI, daripada menjadwalkannya kembali ke threadpool.

Lantas bagaimana kebuntuan ini? Bayangkan Anda hanya memiliki kode ini:

var task = dataSource.ExecuteAsync(_ => 42);
var result = task.Result;

Jadi baris pertama memulai pekerjaan asinkron. Baris kedua kemudian memblokir thread UI . Jadi, ketika runtime ingin menjalankan kembali baris "hasil kembalian" pada UI thread, itu tidak bisa melakukannya sampai Resultselesai. Tetapi tentu saja, Hasil tidak dapat diberikan sampai pengembalian terjadi. Jalan buntu.

Ini mengilustrasikan aturan utama penggunaan TPL: saat Anda menggunakan .ResultUI thread (atau konteks sinkronisasi mewah lainnya), Anda harus berhati-hati untuk memastikan bahwa tidak ada yang bergantung pada Task yang dijadwalkan ke thread UI. Atau kejahatan terjadi.

Jadi apa yang kamu lakukan? Opsi # 1 adalah penggunaan menunggu di mana-mana, tetapi seperti yang Anda katakan, itu bukan opsi. Opsi kedua yang tersedia untuk Anda adalah berhenti menggunakan await. Anda dapat menulis ulang dua fungsi Anda menjadi:

public static Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    return Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });
}

public static Task<ResultClass> GetTotalAsync( ... )
{
    return this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));
}

Apa bedanya? Sekarang tidak ada menunggu di mana pun, jadi tidak ada yang dijadwalkan secara implisit ke thread UI. Untuk metode sederhana seperti ini yang memiliki pengembalian tunggal, tidak ada gunanya melakukan var result = await...; return resultpola " "; hapus saja pengubah async dan teruskan objek tugas secara langsung. Ini lebih sedikit overhead, jika tidak ada yang lain.

Opsi # 3 adalah menentukan bahwa Anda tidak ingin menunggu Anda menjadwalkan kembali ke utas UI, tetapi cukup jadwalkan ke kumpulan utas. Anda melakukan ini dengan ConfigureAwaitmetode, seperti:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var resultTask = this.DBConnection.ExecuteAsync<ResultClass>(
        ds => return ds.Execute("select slow running data into result");

    return await resultTask.ConfigureAwait(false);
}

Menunggu tugas biasanya akan menjadwalkan ke thread UI jika Anda berada di sana; menunggu hasil ContinueAwaitakan mengabaikan konteks apa pun yang Anda buka, dan selalu menjadwalkan ke threadpool. Kelemahan dari ini adalah Anda harus menyebarkan ini di mana-mana di semua fungsi. Hasil Anda bergantung, karena setiap yang terlewat .ConfigureAwaitmungkin menjadi penyebab kebuntuan lain.


6
BTW, pertanyaannya tentang ASP.NET, jadi tidak ada utas UI. Tetapi masalah dengan kebuntuan persis sama, karena ASP.NET SynchronizationContext.
svick

Itu menjelaskan banyak, karena saya memiliki kode .Net 4 serupa yang tidak memiliki masalah tetapi menggunakan TPL tanpa async/ awaitkata kunci.
Keith


Jika ada yang mencari kode VB.net (seperti saya) dijelaskan di sini: docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/…
MichaelDarkBlue


36

Ini adalah asyncskenario jalan buntu campuran klasik , seperti yang saya jelaskan di blog saya . Jason menjelaskannya dengan baik: secara default, "konteks" disimpan di setiap awaitdan digunakan untuk melanjutkan asyncmetode. "Konteks" ini adalah arus SynchronizationContextkecuali itu null, dalam hal ini adalah arus TaskScheduler. Ketika asyncmetode mencoba untuk melanjutkan, pertama kali masuk kembali ke "konteks" yang diambil (dalam hal ini, ASP.NET SynchronizationContext). ASP.NET SynchronizationContexthanya mengizinkan satu utas dalam konteks pada satu waktu, dan sudah ada utas dalam konteks - utas diblokir Task.Result.

Ada dua pedoman yang akan menghindari kebuntuan ini:

  1. Gunakan asyncsepenuhnya. Anda menyebutkan bahwa Anda "tidak dapat" melakukan ini, tetapi saya tidak yakin mengapa tidak. ASP.NET MVC di .NET 4.5 pasti dapat mendukung asynctindakan, dan ini bukanlah perubahan yang sulit untuk dilakukan.
  2. Gunakan ConfigureAwait(continueOnCapturedContext: false)sebanyak mungkin. Ini mengesampingkan perilaku default untuk melanjutkan pada konteks yang diambil.

Apakah ConfigureAwait(false)menjamin bahwa fungsi saat ini dilanjutkan pada konteks yang berbeda?
chue x

Kerangka MVC mendukungnya, tetapi ini adalah bagian dari aplikasi MVC yang sudah ada dengan banyak JS sisi klien yang sudah ada. Saya tidak dapat dengan mudah beralih ke suatu asynctindakan tanpa merusak cara kerjanya di sisi klien. Saya pasti berencana untuk menyelidiki opsi itu dalam jangka panjang.
Keith

Hanya untuk mengklarifikasi komentar saya - Saya ingin tahu apakah menggunakan ConfigureAwait(false)pohon panggilan akan menyelesaikan masalah OP.
chue x

3
@Keith: Membuat tindakan MVC sama asyncsekali tidak memengaruhi sisi klien. Saya menjelaskan ini di posting blog lain, asyncTidak Mengubah Protokol HTTP .
Stephen Cleary

1
@Keith: Itu normal untuk async"tumbuh" melalui basis kode. Jika metode pengontrol Anda mungkin bergantung pada operasi asinkron, maka metode kelas dasar harus dikembalikan Task<ActionResult>. Mengalihkan proyek besar menjadi asyncselalu canggung karena mencampur asyncdan menyinkronkan kode itu sulit dan rumit. asyncKode murni jauh lebih sederhana.
Stephen Cleary

12

Saya berada dalam situasi kebuntuan yang sama tetapi dalam kasus saya memanggil metode asinkron dari metode sinkronisasi, yang berhasil untuk saya adalah:

private static SiteMetadataCacheItem GetCachedItem()
{
      TenantService TS = new TenantService(); // my service datacontext
      var CachedItem = Task.Run(async ()=> 
               await TS.GetTenantDataAsync(TenantIdValue)
      ).Result; // dont deadlock anymore
}

apakah ini pendekatan yang bagus?


Solusi ini berfungsi untuk saya juga, tetapi saya tidak yakin apakah itu solusi yang baik atau mungkin rusak di suatu tempat. Siapa pun dapat menjelaskannya
Konstantin Vdovkin

baik akhirnya saya pergi dengan solusi ini dan bekerja di lingkungan yang produktif tanpa masalah .....
Danilow

1
Saya pikir Anda mengalami penurunan kinerja menggunakan Task.Run. Dalam pengujian saya, Task.Run hampir menggandakan waktu eksekusi untuk permintaan http 100ms.
Timothy Gonzalez

1
yang masuk akal, Anda membuat tugas baru untuk
menggabungkan

Fantastis ini bekerja untuk saya juga, kasus saya juga disebabkan oleh metode sinkron yang memanggil metode asinkron. Terima kasih!
Leonardo Spina

4

Hanya untuk menambah jawaban yang diterima (tidak cukup perwakilan untuk berkomentar), saya mengalami masalah ini muncul saat memblokir penggunaan task.Result, meskipun setiap awaitdi bawahnya ConfigureAwait(false), seperti dalam contoh ini:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

Masalah sebenarnya terletak pada kode perpustakaan eksternal. Metode pustaka asinkron mencoba melanjutkan dalam konteks sinkronisasi panggilan, tidak peduli bagaimana saya mengonfigurasi menunggu, yang menyebabkan kebuntuan.

Jadi, jawabannya adalah menggulung versi saya sendiri dari kode perpustakaan eksternal ExternalLibraryStringAsync, sehingga itu akan memiliki properti lanjutan yang diinginkan.


jawaban yang salah untuk tujuan sejarah

Setelah banyak kesakitan dan kesedihan, saya menemukan solusi yang terkubur dalam posting blog ini (Ctrl-f untuk 'deadlock'). Ini berputar di sekitar penggunaan task.ContinueWith, bukan telanjang task.Result.

Contoh deadlock sebelumnya:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

Hindari kebuntuan seperti ini:

public Foo GetFooSynchronous
{
    var foo = new Foo();
    GetInfoAsync()  // ContinueWith doesn't run until the task is complete
        .ContinueWith(task => foo.Info = task.Result);
    return foo;
}

private async Task<string> GetInfoAsync
{
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

Untuk apa suara negatifnya? Solusi ini berhasil untuk saya.
Cameron Jeffers

Anda mengembalikan objek sebelum Taskselesai, dan memberi pemanggil sarana untuk menentukan kapan mutasi objek yang dikembalikan benar-benar terjadi.
Pelayanan

hmm ya saya mengerti. Jadi haruskah saya mengekspos semacam metode "tunggu sampai tugas selesai" yang menggunakan pemblokiran while loop (atau sesuatu seperti itu) secara manual? Atau mengemas blok seperti itu ke dalam GetFooSynchronousmetode?
Cameron Jeffers

1
Jika Anda melakukannya, itu akan menemui jalan buntu. Anda harus melakukan asinkronisasi sepenuhnya dengan mengembalikan a Taskalih - alih memblokir.
Pelayanan

Sayangnya itu bukan pilihan, kelas menerapkan antarmuka sinkron yang tidak dapat saya ubah.
Cameron Jeffers

0

jawaban cepat: ubah baris ini

ResultClass slowTotal = asyncTask.Result;

untuk

ResultClass slowTotal = await asyncTask;

Mengapa? Anda sebaiknya tidak menggunakan .result untuk mendapatkan hasil dari tugas di dalam sebagian besar aplikasi kecuali aplikasi konsol jika Anda melakukannya program Anda akan hang saat sampai di sana

Anda juga dapat mencoba kode di bawah ini jika Anda ingin menggunakan .Result

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.