Bagaimana saya bisa mendiagnosis async / menunggu deadlock?


24

Saya bekerja dengan basis kode baru yang banyak menggunakan async / menunggu. Sebagian besar orang di tim saya juga cukup baru untuk async / menunggu. Kami umumnya cenderung berpegang pada Praktik Terbaik sebagaimana Ditentukan oleh Microsoft , tetapi umumnya membutuhkan konteks kami untuk mengalir melalui panggilan async dan bekerja dengan perpustakaan yang tidak ConfigureAwait(false).

Gabungkan semua hal itu dan kami mengalami kebuntuan async yang dijelaskan dalam artikel ... mingguan. Mereka tidak muncul selama pengujian unit, karena sumber data kami yang diejek (biasanya via Task.FromResult) tidak cukup untuk memicu kebuntuan. Jadi selama uji runtime atau integrasi, beberapa panggilan layanan keluar untuk makan siang dan tidak pernah kembali. Itu membunuh server, dan umumnya membuat kekacauan.

Masalahnya adalah bahwa melacak di mana kesalahan itu dibuat (biasanya tidak async sepanjang jalan) umumnya melibatkan inspeksi kode manual, yang memakan waktu dan tidak mampu otomatis.

Apa cara yang lebih baik untuk mendiagnosis apa yang menyebabkan kebuntuan?


1
Pertanyaan bagus; Saya sendiri sudah bertanya-tanya. Sudahkah Anda membaca koleksi asyncartikel orang ini ?
Robert Harvey

@RobertHarvey - mungkin tidak semua, tapi saya sudah membaca beberapa. Lebih "Pastikan untuk melakukan dua / tiga hal ini di mana-mana atau kode Anda akan mati parah saat runtime".
Telastyn

Apakah Anda terbuka untuk menjatuhkan async atau mengurangi penggunaannya ke poin yang paling menguntungkan? Async IO tidak semuanya atau tidak sama sekali.
usr

1
Jika Anda dapat mereproduksi kebuntuan, tidak bisakah Anda melihat jejak stack untuk melihat panggilan pemblokiran?
svick

2
Jika masalahnya "tidak asinkron", maka itu berarti bahwa setengah dari kebuntuan adalah kebuntuan tradisional dan harus terlihat dalam tumpukan jejak dari konteks konteks sinkronisasi.
svick

Jawaban:


4

Oke - Saya tidak yakin apakah yang berikut ini akan membantu Anda, karena saya membuat beberapa asumsi dalam mengembangkan solusi yang mungkin atau mungkin tidak benar dalam kasus Anda. Mungkin "solusi" saya terlalu teoretis dan hanya berfungsi untuk contoh buatan - saya belum melakukan pengujian di luar hal-hal di bawah ini.
Selain itu, saya akan melihat solusi berikut lebih dari solusi nyata tetapi mengingat kurangnya tanggapan saya pikir itu mungkin masih lebih baik daripada tidak sama sekali (saya terus menonton pertanyaan Anda menunggu solusi, tetapi tidak melihat satu diposting saya mulai bermain sekitar dengan masalah ini).

Tetapi cukup mengatakan: Katakanlah kita memiliki layanan data sederhana yang dapat digunakan untuk mengambil integer:

public interface IDataService
{
    Task<int> LoadMagicInteger();
}

Implementasi sederhana menggunakan kode asinkron:

public sealed class CustomDataService
    : IDataService
{
    public async Task<int> LoadMagicInteger()
    {
        Console.WriteLine("LoadMagicInteger - 1");
        await Task.Delay(100);
        Console.WriteLine("LoadMagicInteger - 2");
        var result = 42;
        Console.WriteLine("LoadMagicInteger - 3");
        await Task.Delay(100);
        Console.WriteLine("LoadMagicInteger - 4");
        return result;
    }
}

Sekarang, muncul masalah, jika kita menggunakan kode "salah" seperti yang diilustrasikan oleh kelas ini. Foosalah mengakses Task.Resultalih-alih awaithasil seperti Barhalnya:

public sealed class ClassToTest
{
    private readonly IDataService _dataService;

    public ClassToTest(IDataService dataService)
    {
        this._dataService = dataService;
    }

    public async Task<int> Foo()
    {
        var result = this._dataService.LoadMagicInteger().Result;
        return result;
    }
    public async Task<int> Bar()
    {
        var result = await this._dataService.LoadMagicInteger();
        return result;
    }
}

Yang kami (Anda) butuhkan sekarang adalah cara untuk menulis tes yang berhasil ketika memanggil Bartetapi gagal saat memanggil Foo(setidaknya jika saya mengerti pertanyaan dengan benar ;-)).

Saya akan membiarkan kode berbicara; inilah yang saya hasilkan (menggunakan tes Visual Studio, tetapi harus bekerja menggunakan NUnit juga):

DataServiceMockmemanfaatkan TaskCompletionSource<T>. Ini memungkinkan kita untuk mengatur hasil pada titik yang ditentukan dalam uji coba yang mengarah ke tes berikut. Perhatikan bahwa kami menggunakan delegasi untuk mengembalikan kembali TaskCompletionSource ke pengujian. Anda mungkin juga memasukkan ini ke dalam metode Inisialisasi pengujian dan gunakan properti.

TaskCompletionSource<int> tcs = null;
this._dataService.LoadMagicIntegerMock = t => tcs = t;

Task<int> task = null;
TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Foo());

tcs.TrySetResult(42);

var result = task.Result;
Assert.AreEqual(42, result);

this._end = true;

Apa yang terjadi di sini adalah bahwa kami terlebih dahulu memverifikasi bahwa kami dapat meninggalkan metode tanpa memblokir (ini tidak akan berfungsi jika seseorang mengakses Task.Result- dalam hal ini kami akan mengalami batas waktu karena hasil tugas tidak tersedia sampai setelah metode telah kembali ).
Kemudian, kita mengatur hasilnya (sekarang metode dapat dijalankan) dan kami memverifikasi hasilnya (di dalam unit test kita dapat mengakses Task.Result karena kita benar - benar ingin pemblokiran terjadi).

Kelas tes lengkap - BarTestberhasil dan FooTestgagal seperti yang diinginkan.

[TestClass]
public class UnitTest1
{
    private DataServiceMock _dataService;
    private ClassToTest _instance;
    private bool _end;

    [TestInitialize]
    public void Initialize()
    {
        this._dataService = new DataServiceMock();
        this._instance = new ClassToTest(this._dataService);

        this._end = false;
    }
    [TestCleanup]
    public void Cleanup()
    {
        Assert.IsTrue(this._end);
    }

    [TestMethod]
    public void FooTest()
    {
        TaskCompletionSource<int> tcs = null;
        this._dataService.LoadMagicIntegerMock = t => tcs = t;

        Task<int> task = null;
        TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Foo());

        tcs.TrySetResult(42);

        var result = task.Result;
        Assert.AreEqual(42, result);

        this._end = true;
    }
    [TestMethod]
    public void BarTest()
    {
        TaskCompletionSource<int> tcs = null;
        this._dataService.LoadMagicIntegerMock = t => tcs = t;

        Task<int> task = null;
        TaskTestHelper.AssertDoesNotBlock(() => task = this._instance.Bar());

        tcs.TrySetResult(42);

        var result = task.Result;
        Assert.AreEqual(42, result);

        this._end = true;
    }
}

Dan kelas pembantu kecil untuk menguji kebuntuan / batas waktu:

public static class TaskTestHelper
{
    public static void AssertDoesNotBlock(Action action, int timeout = 1000)
    {
        var timeoutTask = Task.Delay(timeout);
        var task = Task.Factory.StartNew(action);

        Task.WaitAny(timeoutTask, task);

        Assert.IsTrue(task.IsCompleted);
    }
}

Jawaban bagus. Saya berencana untuk mencoba kode Anda sendiri ketika saya punya waktu (saya tidak benar-benar tahu pasti apakah itu berfungsi atau tidak), tetapi pujian dan upvote untuk usaha tersebut.
Robert Harvey

-2

Berikut adalah strategi yang saya gunakan dalam aplikasi besar dan sangat, sangat multithread:

Pertama, Anda memerlukan beberapa struktur data di sekitar mutex (sayangnya) dan tidak membuat direktori panggilan sinkronisasi. Dalam struktur data itu, ada tautan ke mutex yang sebelumnya dikunci. Setiap mutex memiliki "level" mulai dari 0, yang Anda tetapkan saat mutex dibuat dan tidak pernah dapat berubah.

Dan aturannya adalah: Jika sebuah mutex terkunci, Anda hanya boleh mengunci mutex lain di tingkat yang lebih rendah. Jika Anda mengikuti aturan itu, maka Anda tidak dapat memiliki deadlock. Ketika Anda menemukan pelanggaran, aplikasi Anda masih aktif dan berfungsi baik.

Ketika Anda menemukan pelanggaran, ada dua kemungkinan: Anda mungkin telah menetapkan level yang salah. Anda mengunci A diikuti dengan mengunci B, jadi B seharusnya memiliki level yang lebih rendah. Jadi, Anda memperbaiki level dan coba lagi.

Kemungkinan lainnya: Anda tidak dapat memperbaikinya. Beberapa kode Anda mengunci A diikuti dengan mengunci B, sementara beberapa kode lainnya mengunci B diikuti dengan mengunci A. Tidak ada cara untuk menetapkan level untuk mengizinkan ini. Dan tentu saja ini adalah kebuntuan potensial: Jika kedua kode berjalan secara bersamaan pada utas yang berbeda, ada kemungkinan kebuntuan.

Setelah memperkenalkan ini, ada fase yang agak pendek di mana level harus disesuaikan, diikuti oleh fase yang lebih panjang di mana deadlock potensial ditemukan.


4
Maaf, bagaimana hal itu berlaku untuk perilaku async / menunggu? Saya tidak bisa secara realistis menyuntikkan struktur manajemen mutex kustom ke Perpustakaan Paralel Tugas.
Telastyn

-3

Apakah Anda menggunakan Async / Menunggu sehingga Anda dapat memparalelkan panggilan mahal seperti ke basis data? Bergantung pada jalur eksekusi di DB ini mungkin tidak mungkin.

Cakupan uji coba dengan async / menunggu dapat menjadi tantangan dan tidak ada yang seperti penggunaan produksi nyata untuk menemukan bug. Satu pola yang dapat Anda pertimbangkan adalah meneruskan ID korelasi dan mencatatnya di tumpukan, kemudian memiliki batas waktu cascading yang mencatat kesalahan. Ini lebih merupakan pola SOA tetapi setidaknya itu akan memberi Anda rasa dari mana asalnya. Kami menggunakan ini dengan Splunk untuk menemukan kebuntuan.

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.