AsyncDispose'teki istisnalarla başa çıkmanın uygun yolu


20

Yeni .NET Core 3'lere geçiş sırasında IAsynsDisposableaşağıdaki sorunla karşılaştım.

Sorunun özü: DisposeAsyncbir istisna atarsa, bu istisna await using-block içine atılan istisnaları gizler .

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");
    }
}

Yakalanan şey, AsyncDisposeatıldığında-istisna ve içeriden istisna await usingsadece AsyncDisposeatmazsa.

Ancak bunu başka bir şekilde tercih ederim: await usingmümkünse bloktan istisna almak ve DisposeAsync-sadece await usingblok başarıyla bittiğinde istisna .

Gerekçe: Sınıfımın Dbazı ağ kaynaklarıyla çalıştığını ve bazı bildirimlerin uzaktan abone olduğunu hayal edin . İçindeki kod await usingyanlış bir şey yapabilir ve iletişim kanalında başarısız olabilir, bundan sonra Dispose'deki iletişimi zarif bir şekilde kapatmaya çalışan kod (örneğin, bildirimlerden çıkma) da başarısız olur. Ama ilk istisna bana sorunla ilgili gerçek bilgileri veriyor, ikincisi ise sadece ikincil bir problem.

Diğer durumda, ana kısım bittiğinde ve bertaraf başarısız olduğunda, gerçek sorun içeridedir DisposeAsync, bu nedenle istisna DisposeAsyncilgili olanıdır. Bu, içerideki tüm istisnaları bastırmanın DisposeAsynciyi bir fikir olmaması gerektiği anlamına gelir .


Eşzamansız durumda da aynı sorun olduğunu biliyorum: istisna finallygeçersiz kılınır try, bu yüzden atmak tavsiye edilmez Dispose(). Ancak ağa erişen sınıflarla, kapatma yöntemlerindeki istisnaları bastırmak hiç de iyi görünmüyor.


Aşağıdaki yardımcıyla soruna geçici bir çözüm bulmak mümkündür:

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 { }
        }
    }
}

ve onu gibi kullan

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

ki bu çirkin bir şeydir (ve kullanım bloğundaki erken dönüşler gibi şeylere izin vermez).

await usingMümkünse iyi, kanonik bir çözüm var mı ? İnternet'teki aramam bu sorunu tartışırken bile bulamadı.


1
" Ama ağ erişim sınıfları ile kapatma yöntemlerinde istisnaları bastırmak hiç iyi görünmüyor " - Ben çoğu ağ BLC sınıfları Closebu nedenle ayrı bir yöntem var düşünüyorum . Muhtemelen aynısını yapmak akıllıca olacaktır: CloseAsyncişleri güzelce kapatmaya çalışır ve başarısızlığa neden olur. DisposeAsyncsadece elinden gelenin en iyisini yapar ve sessizce başarısız olur.
canton7

@ canton7: ​​Çalıştırmak CloseAsynciçin ekstra önlemler almam gereken ayrı bir yol var . Eğer sadece using-block'un sonuna koyarsam, erken iadelerde vb. Ama fikir umut verici görünüyor.
Vlad

Birçok kodlama standardının erken geri dönüşleri yasaklamasının bir nedeni vardır :) Ağ oluşturma söz konusu olduğunda, biraz açık olmak IMO'nun kötü bir şey değildir. Disposeher zaman "İşler yanlış gitmiş olabilir: sadece durumu iyileştirmek için elinden geleni yap, ama daha da kötüleştirme" ve neden AsyncDisposefarklı olması gerektiğini anlamıyorum .
canton7

@ canton7: ​​İstisnaları olan bir dilde, her ifade erken bir dönüş olabilir: - \
Vlad

Doğru, ama bunlar istisnai olacak . Bu durumda, DisposeAsynctoparlamak için elinden gelenin en iyisini yapmak, ancak atmamak doğru şeydir. Kasıtlı erken dönüşlerden bahsediyordunuz , kasıtlı erken dönüş yanlışlıkla bir çağrıyı yanlışlıkla atlayabilir CloseAsync: bunlar birçok kodlama standardı tarafından yasaklananlardır.
canton7

Yanıtlar:


3

Yüzeyleştirmek istediğiniz istisnalar vardır (mevcut isteği yarıda keser veya süreci aşağı çeker) ve tasarımınızın bazen gerçekleşmesini beklediği ve bunları kaldırabileceğiniz istisnalar vardır (örneğin, yeniden dene ve devam et).

Ancak bu iki tür arasında ayrım yapmak, kodun nihai arayanıdır - kararı arayana bırakmak için istisnaların hepsi budur.

Bazen arayan, orijinal kod bloğundan istisnayı ve bazen Dispose. Hangisinin öncelikli olacağına karar vermek için genel bir kural yoktur. CLR, senkronizasyon ile senkronize olmayan davranış arasında en azından (belirttiğiniz gibi) tutarlıdır.

Şimdi AggregateExceptionbirden fazla istisnayı temsil etmek zorunda olduğumuz belki de talihsiz bir durumdur, bunu çözmek için sonradan güçlendirilemez. yani uçuşta bir istisna varsa ve bir başkası fırlatılırsa, bir AggregateException. catchMekanizma yazarsanız o kadar modifiye edilebilir catch (MyException)o zaman herhangi yakalayacak AggregateExceptiontüründe bir istisna içerdiğini MyException. Yine de bu fikirden kaynaklanan çeşitli başka komplikasyonlar var ve muhtemelen bu kadar temel bir şeyi değiştirmek çok riskli.

Sen kalkındırmak olabilir UsingAsyncbir değerin erken dönüşü desteklemek için:

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;
}

Bu yüzden doğru anlıyorum: fikriniz bazı durumlarda sadece standart await usingkullanılabiliyor (bu, DisposeAsync'in ölümcül olmayan bir durumda atmayacağı yer) ve benzer bir yardımcı UsingAsyncdaha uygun (DisposeAsync'in atması muhtemelse) ? (Tabii ki, UsingAsyncher şeyi körü körüne yakalamak için değil, sadece ölümcül olmayan (ve Eric Lippert'in kullanımında alınmamış) değiştirmek zorundayım .)
Vlad

@Evet evet - doğru yaklaşım tamamen bağlama bağlıdır. Ayrıca, UseAsync'in, yakalanması gerekip gerekmediğine göre istisna türlerinin küresel olarak doğru kategorize edilmesini kullanmak için bir kez yazılamayacağını unutmayın. Yine bu duruma bağlı olarak farklı bir karar alınması gerektiğidir. Eric Lippert bu kategorilerden bahsettiğinde, istisna türleri hakkında gerçekler değildir. İstisna türü başına kategori tasarımınıza bağlıdır. Bazen tasarımla bir IOException beklenir, bazen de beklenmez.
Daniel Earwicker

4

Belki de bunun neden olduğunu zaten biliyorsunuz, ancak yazılmaya değer. Bu davranış belirli değildir await using. Düz bir usingblokla da olur. Ben Dispose()burada söylerken , hepsi de geçerlidir DisposeAsync().

Bir usingblok, belgelerin açıklamalar bölümünde belirtildiği gibi, bir try/ finallyblok için sadece sözdizimsel şekerdir . Gördüğünüz şey , bir istisnadan sonra bile blok her zaman çalışıyor çünkü olur . Dolayısıyla, bir istisna oluşursa ve blok yoksa, istisna blok çalışana kadar beklemeye alınır ve sonra istisna atılır. Ancak bir istisna meydana gelirse , eski istisnayı asla göremezsiniz.finallycatchfinallyfinally

Bunu şu örnekle görebilirsiniz:

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

Fark etmez olsun Dispose()veya DisposeAsync()içeride denir finally. Davranış aynıdır.

İlk düşüncem: İçine atma Dispose(). Ancak Microsoft'un kendi kodlarını inceledikten sonra buna bağlı olduğunu düşünüyorum.

Örneğin, bunların uygulanmasına bir göz atın FileStream. Hem senkronize Dispose()yöntem hem DisposeAsync()de aslında istisnalar atabilir. Senkron Dispose(), kasıtlı olarak bazı istisnaları yok sayar, ancak hepsini değil.

Ama bence sınıfınızın doğasını dikkate almak önemlidir. Bir de FileStream, örneğin, Dispose()bir dosya sistemi tampon silinecektir. Bu çok önemli bir görev ve bunun başarısız olup olmadığını bilmeniz gerekiyor . Bunu görmezden gelemezsin.

Ancak, diğer nesne türlerinde, aradığınızda Dispose(), artık nesneyi gerçekten kullanamazsınız. Arayan Dispose()gerçekten sadece "bu nesne benim için öldü" demektir. Belki de ayrılan hafızayı temizler, ancak başarısızlık uygulamanızın çalışmasını hiçbir şekilde etkilemez. Bu durumda, içindeki istisnayı göz ardı etmeye karar verebilirsiniz Dispose().

Ancak her durumda, içindeki bir istisna usingveya gelen bir istisna arasında ayrım yapmak istiyorsanız Dispose(), bloğunuzun hem içinde hem de dışında bir try/ catchbloğuna ihtiyacınız vardır using:

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
}

Ya da sadece kullanamazsın using. Aşağıdaki durumlarda bir istisna yakaladığınız bir try/ catch/ finallybloğunu yazın 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
    }
}

3
Btw, source.dot.net (.NET Core) / referenceource.microsoft.com (.NET Framework), GitHub'dan daha kolay
gözlenir

Cevabınız için teşekkür ederim! Asıl nedenin ne olduğunu biliyorum (denemede / nihayet ve eşzamanlı durumdan bahsettim). Şimdi teklifiniz hakkında. Bir catch using bloğun olur genellikle istisna işleme yere uzak yapıldığı için değil yardım usingöylesine içine taşınması, bloğun kendisi usinggenellikle çok pratik değildir. Hayır kullanma hakkında using- önerilen geçici çözümden gerçekten daha iyi mi?
Vlad

2
@ canton7 Harika! Referenceource.microsoft.com'un farkındaydım , ancak .NET Core için bir eşdeğer olduğunu bilmiyordum. Teşekkürler!
Gabriel Luci

@Vlad "Daha iyi" sadece cevaplayabileceğiniz bir şeydir. Başka birinin kodunu okuyor olsaydım, try/ catch/ finallybloğunu görmeyi tercih ederim, çünkü ne yaptığını okumak zorunda kalmadan ne yaptığını hemen anlardı AsyncUsing. Ayrıca erken dönüş yapma seçeneğine de sahip olursunuz. Ayrıca ekstra bir CPU maliyeti olacak AwaitUsing. Küçük olurdu, ama orada.
Gabriel Luci

2
Bu sadece bir araç @PauloMorgado Dispose()atmak olmamalı çünkü birden fazla kez denir. Microsoft'un kendi uygulamaları, bu yanıtta gösterdiğim gibi, istisnalar ve iyi bir neden olabilir. Ancak, hiç kimse normalde atmayı beklemediğinden, mümkün olduğunca kaçınmanız gerektiğine katılıyorum.
Gabriel Luci

4

kullanmak etkili bir şekilde İstisna İşleme Kodu (denemek için sözdizimi şekeri ... sonunda ... Dispose ()).

İstisna işleme kodunuz İstisnalar atıyorsa, bir şey kraliyet olarak yakalanır.

Seni oraya götürmek için başka ne olursa olsun, artık gerçekten materyal değil. Hatalı İstisna işleme kodu, olası tüm istisnaları bir şekilde gizler. Kural dışı durum işleme kodu, mutlak önceliğe sahip olan sabit olmalıdır. Bu olmadan, gerçek sorun için asla yeterli hata ayıklama verisi elde edemezsiniz. Çok sık yanlış yapıldığını görüyorum. Çıplak işaretçilerle uğraşmak kadar yanlış anlaşılması kolaydır. Sıklıkla, tematik olarak bağladığım, tasarımın altta yatan tasarım kavramlarına yardımcı olabilecek iki makale var:

İstisna sınıflandırmasına bağlı olarak, İstisna İşleme / Dipoz kodunuz bir İstisna atarsa ​​yapmanız gerekenler şunlardır:

Ölümcül, Boneheaded ve Vexing için çözüm aynıdır.

Dışsal İstisnalardan ciddi maliyetle bile kaçınılmalıdır. Biz hala günlük kullandığımız bir sebep yoktur dosyaları log sonra yerine veritabanlarını istisnaları günlüğe - DB Opeartions Eksojen bir sorunla karşılaşmasını eğilimli sadece yoludur. Dosya tutamacı, tüm çalışma zamanı açık tutmak sakıncası bile ben bile günlük dosyaları.

Bir bağlantıyı kapatmanız gerekiyorsa, diğer uç hakkında fazla endişelenmeyin. UDP'nin yaptığı gibi ele alın: "Bilgileri göndereceğim, ancak diğer tarafın aldığını umursamıyorum." Elden çıkarma, üzerinde çalıştığınız istemci tarafındaki / taraftaki kaynakları temizlemekle ilgilidir.

Onları bilgilendirmeye çalışabilirim. Ama Sunucu / FS Tarafındaki şeyleri temizlemek mi? Yani ne onların zaman aşımları ve onların istisna işleme sorumludur.


Yani teklifiniz, bağlantıdaki istisnaları bastırmaya etkili bir şekilde kayboluyor, değil mi?
Vlad

@ Dışsal olanlar? Elbette. Dipose / Finalizer kendi taraflarını temizlemek için orada. Şansı nedeniyle istisna conneciton Örneğini kapatırken, bunu artık becaue yapmak vardır sahip zaten onlara bir çalışma bağlantısı. Ve önceki "Bağlantı Yok" Özel Durumunu işlerken "Bağlantı Yok" İstisnası elde etmenin ne anlamı olurdu? Tüm dışsal İstisnaları yoksayarsanız veya hedefe yaklaşsa bile tek bir "Yo, ben bu bağlantıyı kapatıyorum" gönderirsiniz. Afaik Dispose'in varsayılan uygulamaları zaten bunu yapıyor.
Christopher

@Vlad: Hatırladım, asla istisnalar atmamanız gereken bir sürü şey var (Ölümcül olanlar hariç). Tür Başlatıcılar listede çok yukarı. Dispose ayrıca bunlardan biridir: "Kaynakların her zaman uygun şekilde temizlenmesini sağlamak için, bir Dispose yöntemi bir istisna atmadan birden çok kez çağrılabilir olmalıdır." docs.microsoft.com/tr-tr/dotnet/standard/garbage-collection/…
Christopher

@ Ölümcül İstisnaların Olasılığı? Bunları her zaman riske atmak zorundayız ve asla "İmha Et" çağrısının ötesine geçmemeliyiz. Ve bunlarla gerçekten hiçbir şey yapmamalıyız. Aslında herhangi bir belgede bahsedilmeden giderler. | Boneheaded İstisnaları? Daima onları düzeltin. | Vexing istisnaları, TryParse () 'de olduğu gibi yutma / taşıma için birincil adaylardır. Eksojen? Ayrıca her zaman handeled olmalıdır. Genellikle kullanıcıya bunlardan bahsetmek ve onları günlüğe kaydetmek istersiniz. Ama aksi takdirde, sürecinizi öldürmeye değmezler.
Christopher

@Vlad SqlConnection.Dispose () 'ye baktım. Bağlantının bittiği hakkında Sunucuya hiçbir şey göndermeyi bile umursamıyor . Yani hala sonucunda gerçekleşebilir NativeMethods.UnmapViewOfFile();ve NativeMethods.CloseHandle(). Ancak bunlar dışarıdan ithal edilir. Herhangi bir dönüş değeri veya bu ikisinin karşılaşabileceği her şey etrafında uygun bir .NET İstisnası elde etmek için kullanılabilecek başka bir şey kontrol yoktur. Bu yüzden şiddetle alıyorum, SqlConnection.Dispose (bool) sadece umursamıyor. | Kapat aslında çok güzel, aslında sunucu anlatıyor. Aramadan önce atın.
Christopher

1

AggregateException özelliğini kullanmayı deneyebilir ve kodunuzu aşağıdaki gibi değiştirebilirsiniz:

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

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.