Cara yang tepat untuk menangani pengecualian di AsyncDispose

20

Selama beralih ke .NET Core 3 yang baru IAsynsDisposable, saya menemukan masalah berikut.

Inti dari masalah: jika DisposeAsyncmelempar pengecualian, pengecualian ini menyembunyikan setiap pengecualian yang dilemparkan ke dalam await using-block.

class Program 
{
    static async Task Main()
    {
        try
        {
            await using (var d = new D())
            {
                throw new ArgumentException("I'm inside using");
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message); // prints I'm inside dispose
        }
    }
}

class D : IAsyncDisposable
{
    public async ValueTask DisposeAsync()
    {
        await Task.Delay(1);
        throw new Exception("I'm inside dispose");
    }
}

Yang tertangkap adalah AsyncDispose-eksepsi jika dilempar, dan pengecualian dari dalam await usinghanya jika AsyncDisposetidak melempar.

Namun saya lebih suka sebaliknya: mendapatkan pengecualian dari await usingblok jika memungkinkan, dan DisposeAsync-kecualian hanya jika await usingblok selesai dengan sukses.

Dasar Pemikiran: Bayangkan bahwa kelas saya Dbekerja dengan beberapa sumber daya jaringan dan berlangganan untuk beberapa notifikasi jarak jauh. Kode di dalam await usingdapat melakukan sesuatu yang salah dan gagal saluran komunikasi, setelah itu kode dalam Buang yang mencoba menutup komunikasi dengan anggun (misalnya, berhenti berlangganan pemberitahuan) akan gagal juga. Tetapi pengecualian pertama memberi saya informasi nyata tentang masalah tersebut, dan yang kedua hanyalah masalah sekunder.

Dalam kasus lain ketika bagian utama dijalankan dan pembuangan gagal, masalah sebenarnya ada di dalam DisposeAsync, jadi pengecualian dari DisposeAsyncadalah yang relevan. Ini berarti bahwa hanya menekan semua pengecualian di dalam DisposeAsyncseharusnya bukan ide yang baik.


Saya tahu bahwa ada masalah yang sama dengan kasus non-async: pengecualian dalam finallymengabaikan pengecualian try, itu sebabnya tidak disarankan untuk melempar Dispose(). Tetapi dengan kelas yang mengakses jaringan menekan pengecualian dalam metode penutupan tidak terlihat bagus sama sekali.


Dimungkinkan untuk mengatasi masalah dengan pembantu berikut:

static class AsyncTools
{
    public static async Task UsingAsync<T>(this T disposable, Func<T, Task> task)
            where T : IAsyncDisposable
    {
        bool trySucceeded = false;
        try
        {
            await task(disposable);
            trySucceeded = true;
        }
        finally
        {
            if (trySucceeded)
                await disposable.DisposeAsync();
            else // must suppress exceptions
                try { await disposable.DisposeAsync(); } catch { }
        }
    }
}

dan menggunakannya seperti

await new D().UsingAsync(d =>
{
    throw new ArgumentException("I'm inside using");
});

yang agak jelek (dan melarang hal-hal seperti pengembalian awal di dalam blok menggunakan).

Apakah ada solusi kanonik yang baik, dengan await usingjika memungkinkan? Pencarian saya di internet bahkan tidak membahas masalah ini.

Vlad
sumber
1
" Tetapi dengan kelas yang mengakses jaringan menekan pengecualian dalam metode penutupan tidak terlihat bagus sama sekali " - Saya pikir sebagian besar kelas BLC jaringan memiliki Closemetode terpisah karena alasan ini. Mungkin bijaksana untuk melakukan hal yang sama: CloseAsyncupaya untuk menutup semuanya dengan baik dan gagal. DisposeAsynchanya melakukan yang terbaik, dan gagal diam-diam.
canton7
@ canton7: ​​Ya, memiliki cara yang terpisah CloseAsyncbahwa saya perlu mengambil tindakan pencegahan ekstra untuk membuatnya berjalan. Jika saya hanya meletakkannya di akhir using-block, itu akan dilewati pada pengembalian awal dll (ini adalah apa yang kita inginkan terjadi) dan pengecualian (ini adalah apa yang kita inginkan terjadi). Tapi idenya terlihat menjanjikan.
Vlad
Ada alasan banyak standar pengkodean melarang pengembalian awal :) Di mana jaringan terlibat, menjadi agak eksplisit bukanlah hal yang buruk IMO. Disposeselalu menjadi "Ada yang salah: lakukan saja yang terbaik untuk memperbaiki situasi, tetapi jangan membuatnya lebih buruk", dan saya tidak melihat mengapa AsyncDisposeharus ada perbedaan.
canton7
@ canton7: ​​Nah, dalam bahasa-dengan-pengecualian setiap pernyataan mungkin merupakan pengembalian awal: - \
Vlad
Benar, tapi itu akan luar biasa . Dalam hal itu, DisposeAsyncmelakukan yang terbaik untuk merapikan tetapi tidak melempar adalah hal yang benar untuk dilakukan. Anda berbicara tentang pengembalian awal yang disengaja , di mana pengembalian awal yang disengaja dapat secara tidak sengaja mem-bypass panggilan ke CloseAsync: mereka adalah yang dilarang oleh banyak standar pengkodean.
canton7

Jawaban:

3

Ada pengecualian yang ingin Anda kemukakan (mengganggu permintaan saat ini, atau menghentikan proses), dan ada pengecualian bahwa desain Anda mengharapkan akan terjadi kadang-kadang dan Anda dapat menanganinya (misalnya coba lagi dan lanjutkan).

Tetapi membedakan antara kedua jenis ini tergantung pada penelepon utama kode - ini adalah inti dari semua pengecualian, untuk menyerahkan keputusan kepada penelepon.

Kadang - kadang penelepon akan menempatkan prioritas lebih besar pada memunculkan pengecualian dari blok kode asli, dan kadang - kadang pengecualian dari Dispose. Tidak ada aturan umum untuk memutuskan mana yang harus diprioritaskan. CLR setidaknya konsisten (seperti yang telah Anda catat) antara perilaku sinkronisasi dan non-asinkron.

Sangat disayangkan bahwa sekarang kita harus AggregateExceptionmewakili beberapa pengecualian, tidak dapat dipasang untuk menyelesaikan ini. yaitu jika pengecualian sudah dalam penerbangan, dan yang lain dilemparkan, mereka digabungkan menjadi AggregateException. The catchmekanisme dapat dimodifikasi sehingga jika Anda menulis catch (MyException)maka akan menangkap setiap AggregateExceptionyang mencakup pengecualian dari jenisMyException . Ada berbagai komplikasi lain yang berasal dari ide ini, dan mungkin terlalu berisiko untuk memodifikasi sesuatu yang begitu mendasar sekarang.

Anda dapat meningkatkan Anda UsingAsyncuntuk mendukung pengembalian nilai lebih awal:

public static async Task<R> UsingAsync<T, R>(this T disposable, Func<T, Task<R>> task)
        where T : IAsyncDisposable
{
    bool trySucceeded = false;
    R result;
    try
    {
        result = await task(disposable);
        trySucceeded = true;
    }
    finally
    {
        if (trySucceeded)
            await disposable.DisposeAsync();
        else // must suppress exceptions
            try { await disposable.DisposeAsync(); } catch { }
    }
    return result;
}
Daniel Earwicker
sumber
Jadi saya mengerti benar: ide Anda adalah bahwa dalam beberapa kasus hanya standar yang await usingdapat digunakan (ini adalah tempat DisposeAsync tidak akan melempar dalam kasus non-fatal), dan penolong seperti UsingAsynclebih tepat (jika DisposeAsync cenderung membuang) ? (Tentu saja, saya perlu memodifikasi UsingAsyncagar tidak membabi buta menangkap segalanya, tetapi hanya non-fatal (dan tidak bertulang dalam penggunaan Eric Lippert ).)
Vlad
@Vlad ya - pendekatan yang benar benar-benar tergantung pada konteksnya. Perhatikan juga bahwa UsingAsync tidak dapat ditulis sekali untuk menggunakan beberapa kategorisasi tipe pengecualian yang benar secara global berdasarkan apakah mereka harus ditangkap atau tidak. Sekali lagi ini adalah keputusan yang harus diambil berbeda tergantung situasi. Ketika Eric Lippert berbicara tentang kategori-kategori itu, mereka bukan fakta intrinsik tentang tipe pengecualian. Kategori per jenis pengecualian tergantung pada desain Anda. Terkadang IOException diharapkan oleh desain, terkadang tidak.
Daniel Earwicker
4

Mungkin Anda sudah mengerti mengapa ini terjadi, tetapi perlu diuraikan. Perilaku ini tidak spesifik untuk await using. Itu akan terjadi dengan usingbalok polos juga. Jadi sementara saya katakan di Dispose()sini, itu semua berlaku untukDisposeAsync() juga.

Satu usingblok hanyalah gula sintaksis untuk a try/ finallyblok, seperti yang dikatakan di bagian dokumentasi . Apa yang Anda lihat terjadi karena finallyblok selalu berjalan, bahkan setelah pengecualian. Jadi jika pengecualian terjadi, dan tidak ada catchblok, pengecualian ditunda hingga finallyblok berjalan, dan kemudian pengecualian dilemparkan. Tetapi jika pengecualian terjadi difinally , Anda tidak akan pernah melihat pengecualian lama.

Anda dapat melihat ini dengan contoh ini:

try {
    throw new Exception("Inside try");
} finally {
    throw new Exception("Inside finally");
}

Tidak masalah apakah Dispose()atau DisposeAsync()disebut di dalamfinally . Tingkah lakunya sama.

Pikiran pertama saya adalah: jangan menyerah Dispose(). Tetapi setelah meninjau beberapa kode Microsoft sendiri, saya pikir itu tergantung.

Lihatlah implementasi mereka FileStream, misalnya. Baik Dispose()metode sinkron , dan DisposeAsync()sebenarnya bisa melempar pengecualian. Sinkron Dispose()tidak mengabaikan beberapa pengecualian sengaja, tapi tidak semua.

Tetapi saya pikir penting untuk mempertimbangkan sifat kelas Anda. Dalam FileStream, misalnya, Dispose()akan menyiram buffer ke sistem file. Itu adalah tugas yang sangat penting dan Anda perlu tahu apakah itu gagal . Anda tidak bisa mengabaikannya begitu saja.

Namun, pada jenis objek lainnya, saat Anda menelepon Dispose(), Anda benar-benar tidak lagi menggunakan objek tersebut. Memanggil Dispose()benar-benar hanya berarti "objek ini mati bagiku". Mungkin membersihkan beberapa memori yang dialokasikan, tetapi gagal tidak mempengaruhi pengoperasian aplikasi Anda dengan cara apa pun. Jika demikian, Anda mungkin memutuskan untuk mengabaikan pengecualian di dalam Dispose().

Tetapi bagaimanapun juga, jika Anda ingin membedakan antara pengecualian di dalam usingatau pengecualian yang berasal Dispose(), maka Anda memerlukan try/ catchblok baik di dalam maupun di luar usingblok Anda :

try {
    await using (var d = new D())
    {
        try
        {
            throw new ArgumentException("I'm inside using");
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message); // prints I'm inside using
        }
    }
} catch (Exception e) {
    Console.WriteLine(e.Message); // prints I'm inside dispose
}

Atau Anda tidak bisa menggunakannya using. Tuliskan try/ catch/ finallyblokir diri Anda sendiri, di mana Anda mendapatkan pengecualian di finally:

var d = new D();
try
{
    throw new ArgumentException("I'm inside try");
}
catch (Exception e)
{
    Console.WriteLine(e.Message); // prints I'm inside try
}
finally
{
    try
    {
        if (D != null) await D.DisposeAsync();
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message); // prints I'm inside dispose
    }
}
Gabriel Luci
sumber
3
Btw, source.dot.net (.NET Core) / Referenceource.microsoft.com (.NET Framework) jauh lebih mudah dijelajahi daripada GitHub
canton7
Terima kasih atas jawaban Anda! Saya tahu apa alasan sebenarnya (saya sebutkan coba / akhirnya dan kasus sinkron dalam pertanyaan). Sekarang tentang proposal Anda. Sebuah catch dalam satu usingblok tidak akan membantu karena biasanya pengecualian penanganan dilakukan di suatu tempat jauh dari usingblok itu sendiri, sehingga penanganan di dalam usingbiasanya tidak terlalu praktis. Tentang tidak menggunakan using- apakah ini benar-benar lebih baik daripada solusi yang diusulkan?
Vlad
2
@ canton7 Luar Biasa! Saya mengetahui Referenceource.microsoft.com , tetapi tidak tahu ada yang setara dengan .NET Core. Terima kasih!
Gabriel Luci
@Vlad "Lebih baik" adalah sesuatu yang hanya bisa Anda jawab. Saya tahu jika saya membaca kode orang lain, saya lebih suka melihat try/ catch/ finallyblok karena akan segera jelas apa yang dilakukannya tanpa harus membaca apa yang AsyncUsingsedang dilakukan. Anda juga menyimpan opsi untuk melakukan pengembalian lebih awal. Anda juga akan dikenakan biaya CPU tambahan AwaitUsing. Itu akan kecil, tapi ada di sana.
Gabriel Luci
2
@ PauloMorgado Itu artinya Dispose()tidak boleh melempar karena dipanggil lebih dari sekali. Implementasi Microsoft sendiri dapat membuang pengecualian, dan untuk alasan yang baik, seperti yang saya tunjukkan dalam jawaban ini. Namun, saya setuju bahwa Anda harus menghindarinya jika mungkin karena tidak ada yang biasanya berharap untuk membuangnya.
Gabriel Luci
4

menggunakan secara efektif Kode Penanganan Pengecualian (gula sintaks untuk dicoba ... akhirnya ... Buang ()).

Jika pengecualian Anda menangani kode melempar Pengecualian, ada sesuatu yang rusak secara meriah.

Apa pun yang terjadi bahkan membawa Anda ke sana, tidak benar-benar mater lagi. Kode penanganan Pengecualian yang salah akan menyembunyikan semua kemungkinan pengecualian, dengan satu atau lain cara. Kode penanganan pengecualian harus diperbaiki, yang memiliki prioritas absolut. Tanpa itu, Anda tidak akan pernah mendapatkan data debug yang cukup untuk masalah sebenarnya. Saya melihat kesalahan itu sering dilakukan secara ekstrem. Ini tentang kesalahan yang mudah, seperti menangani pointer telanjang. Begitu sering, ada dua Artikel tentang tautan saya tematik, yang dapat membantu Anda dengan konsep desain yang salah:

Bergantung pada klasifikasi Pengecualian, inilah yang perlu Anda lakukan jika kode Penanganan / Dipose Pengecualian Anda mengeluarkan suatu Pengecualian:

Untuk Fatal, Boneheaded dan Vexing solusinya sama.

Pengecualian eksogen, harus dihindari bahkan dengan biaya serius. Ada alasan mengapa kami masih menggunakan log file daripada mencatat basis data untuk mencatat pengecualian - DB Opeartions adalah cara mudah untuk mengalami masalah eksogen. File log adalah satu-satunya kasus, di mana saya bahkan tidak keberatan jika Anda menyimpan File Handle. Buka seluruh Runtime.

Jika Anda harus menutup koneksi, jangan terlalu khawatir tentang ujung yang lain. Tangani seperti yang dilakukan UDP: "Saya akan mengirim informasi, tetapi saya tidak peduli jika pihak lain yang mendapatkannya." Membuang adalah tentang membersihkan sumber daya di sisi klien / sisi yang Anda kerjakan.

Saya dapat mencoba memberi tahu mereka. Tapi membersihkan barang-barang di Sisi Server / FS? Itulah yang mereka timeout dan mereka penanganan eksepsi bertanggung jawab untuk.

Christopher
sumber
Jadi proposal Anda secara efektif bermuara pada menekan pengecualian pada koneksi dekat, kan?
Vlad
@Vlad yang eksogen? Tentu. Dipose / Finalizer ada untuk membersihkan di sisi mereka sendiri. Kemungkinannya adalah ketika menutup Instance Conneciton karena pengecualian, Anda melakukannya karena Anda tidak lagi memiliki koneksi yang berfungsi dengan mereka. Dan apa gunanya mendapatkan Pengecualian "Tidak ada koneksi" saat menangani Pengecualian "Tanpa Koneksi" sebelumnya? Anda mengirim satu "Yo, I a closure this connection" di mana Anda mengabaikan semua Pengecualian eksogen atau bahkan jika mendekati target. Afaik implementasi standar Buang sudah melakukan itu.
Christopher
@Vlad: Saya ingat, bahwa ada banyak hal yang Anda tidak pernah harus melemparkan pengecualian (Kecuali dari yang fatal Fuse) dari. Jenis Initliaizers berada di urutan teratas dalam daftar. Buang juga salah satu dari mereka: "Untuk membantu memastikan bahwa sumber daya selalu dibersihkan dengan tepat, metode Buang harus dapat dipanggil beberapa kali tanpa mengeluarkan pengecualian." docs.microsoft.com/en-us/dotnet/standard/garbage-collection/…
Christopher
@Vlad Peluang Pengecualian Fatal? Kami selalu harus mengambil risiko itu dan jangan pernah menanganinya di luar "panggil Buang". Dan seharusnya tidak benar-benar melakukan apa pun dengan itu. Mereka sebenarnya pergi tanpa menyebutkan dalam dokumentasi apa pun. | Pengecualian Berulang? Selalu perbaiki mereka. | Pengecualian vexing adalah kandidat utama untuk menelan / menangani, seperti di TryParse () | Eksogen? Juga harus selalu handeled. Seringkali Anda juga ingin memberi tahu pengguna tentang mereka dan mencatatnya. Tetapi sebaliknya, mereka tidak layak untuk membunuh proses Anda.
Christopher
@Vlad Saya mencari SqlConnection.Dispose (). Bahkan tidak peduli untuk mengirim apa pun ke Server tentang koneksi yang berakhir. Sesuatu mungkin masih terjadi sebagai akibat dari NativeMethods.UnmapViewOfFile();dan NativeMethods.CloseHandle(). Tapi itu diimpor dari luar. Tidak ada pemeriksaan nilai pengembalian atau apa pun yang dapat digunakan untuk mendapatkan .NET Exception yang tepat di sekitar apa pun yang mungkin ditemui keduanya. Jadi saya sangat berasumsi, SqlConnection.Dispose (bool) tidak peduli. | Tutup jauh lebih baik, sebenarnya memberitahu server. Sebelum itu panggilan buang.
Christopher
1

Anda dapat mencoba menggunakan AggregateException dan mengubah sesuatu kode Anda seperti ini:

class Program 
{
    static async Task Main()
    {
        try
        {
            await using (var d = new D())
            {
                throw new ArgumentException("I'm inside using");
            }
        }
        catch (AggregateException ex)
        {
            ex.Handle(inner =>
            {
                if (inner is Exception)
                {
                    Console.WriteLine(e.Message);
                }
            });
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
    }
}

class D : IAsyncDisposable
{
    public async ValueTask DisposeAsync()
    {
        await Task.Delay(1);
        throw new Exception("I'm inside dispose");
    }
}

https://docs.microsoft.com/ru-ru/dotnet/api/system.aggregateexception?view=netframework-4.8

https://docs.microsoft.com/ru-ru/dotnet/standard/parallel-programming/exception-handling-task-parallel-library

GDI89
sumber