AsyncDisposeで例外を処理する適切な方法


20

新しい.NET Core 3に切り替えるIAsynsDisposableときに、次の問題に遭遇しました。

問題の中核:DisposeAsync例外がスローされた場合、この例外は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");
    }
}

キャッチされているのは、AsyncDisposeそれがスローされた場合の-exceptionと、スローされawait usingなかった場合にのみ内部からの例外AsyncDisposeです。

ただし、逆の方法を使用await usingすることをおDisposeAsync勧めawait usingします。可能であればブロックから例外を取得し、ブロックが正常に終了した場合にのみ例外を取得します。

理論的根拠:クラスDがいくつかのネットワークリソースで動作し、いくつかの通知をリモートでサブスクライブすることを想像してください。内部のコードawait usingは何か問題を起こして通信チャネルに失敗する可能性があります。その後、Disposeの通信を適切に閉じようとする(たとえば、通知の購読を解除する)コードも失敗します。ただし、最初の例外は問題に関する実際の情報を提供し、2番目の例外は二次的な問題にすぎません。

もう1つのケースでは、メインパートが実行され、破棄が失敗した場合、本当の問題は内部DisposeAsyncにあるため、からの例外DisposeAsyncは関連するものです。これは、内部のすべての例外を抑制するだけDisposeAsyncでは良い考えではないことを意味します。


非非同期の場合にも同じ問題があることを知っていfinallyます。の例外がの例外をオーバーライドするため、tryをスローすることはお勧めしませんDispose()。しかし、ネットワークにアクセスするクラスでは、メソッドを閉じる際の例外を抑制することはまったく見栄えがよくありません。


次のヘルパーで問題を回避することができます。

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

そしてそれを

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

これは一種の醜いものです(また、usingブロック内での早期復帰などは許可されていません)。

await using可能であれば、良い標準的な解決策はありますか?インターネットでの検索では、この問題についての議論すら見つかりませんでした。


1
しかし、ネットワークにアクセスするクラスでは、メソッドを閉じる際の例外を抑制するのはまったく見た目が良くありません」-ほとんどのネットワークBLCクラスには、Closeまさにそのために別のメソッドがあると思います。おそらく同じことをするのが賢明CloseAsyncです。物事をうまく閉じ込めようとし、失敗した場合はスローします。DisposeAsync最善を尽くし、静かに失敗します。
canton7

@ canton7:まあ、CloseAsyncそれを実行するには特別な注意を払う必要があるという別の手段がある。using-block の最後に置くだけの場合、初期のリターンなど(これは私たちが起こしたいことです)と例外(これは私たちが起こしたいことです)ではスキップされます。しかし、アイデアは有望に見えます。
Vlad

多くのコーディング標準が早期のリターンを禁止している理由があります:)ネットワークが関与している場合、少し明示的であることはIMOの悪いことではありません。Dispose常に「物事がうまくいかなかった可能性があります。状況を改善するために最善を尽くしますが、悪化させないでください」であり、なぜAsyncDispose違いがあるのかわかりません。
canton7

@ canton7:まあ、例外のある言語では、すべてのステートメントが早期のリターンになる可能性があります:-\
Vlad

そうですが、それらは例外的です。その場合、DisposeAsync片付けには最善を尽くすが、投げないようにすることは正しいことです。あなたは意図的なアーリーリターンについて話していましたが、意図的なアーリーリターンは誤ってへの呼び出しをバイパスする可能性CloseAsyncがあります。これらは多くのコーディング標準で禁止されているものです。
canton7

回答:


3

表面化したい例外(現在の要求を中断したり、プロセスを停止したりする)があり、設計で予期される例外が時々発生し、それらを処理できる(例:再試行して続行)。

ただし、これらの2つのタイプを区別するのは、コードの最終的な呼び出し元次第です。これが例外であり、呼び出し元に決定を任せます。

呼び出し元は、元のコードブロックからの例外の表示を優先する場合もあれば、からの例外の表示を優先する場合もありますDispose。どちらを優先するかを決定するための一般的なルールはありません。CLRは、同期と非同期の振る舞いの間で少なくとも(一貫性があります)。

おそらく残念なことに、今ではAggregateException複数の例外を表現する必要があり、これを解決するために改造することはできません。つまり、例外がすでに実行中で、別の例外がスローされた場合、それらはに結合されますAggregateExceptioncatchあなたが書く場合ように仕組みを変更することができcatch (MyException)、それがどんなキャッチするAggregateExceptionタイプの例外が含まれていることをMyException。ただし、このアイデアには他にもさまざまな複雑な問題があり、根本的なものを変更するのはリスクが高すぎるでしょう。

UsingAsync値の早期返却をサポートするように改善できます。

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

だから私は正しいことを理解していますか?あなたの考えは、いくつかのケースでは標準await usingのみを使用できるということです(これはDisposeAsyncが致命的でないケースでスローしない場所です)のようなヘルパーUsingAsyncがより適切です(DisposeAsyncがスローする可能性がある場合) ?(もちろん、私はUsingAsyncそれがすべてを盲目的に捕まえないように修正する必要がありますが、致命的ではない(そしてエリックリッペルトの使用法に骨頭がない)だけです。)
Vlad

@Vladはい-正しいアプローチは完全にコンテキストに依存します。また、キャッチする必要があるかどうかに応じて、例外タイプのグローバルに真の分類を使用するようにUsingAsyncを一度記述することはできません。繰り返しになりますが、これは状況に応じて異なる方法で行われる決定です。Eric Lippertがこれらのカテゴリについて話すとき、それらは例外タイプについての本質的な事実ではありません。例外タイプごとのカテゴリは、デザインによって異なります。IOExceptionは、設計によって予期される場合と予期されない場合があります。
Daniel Earwicker

4

なぜこれが起こるのかすでに理解しているかもしれませんが、詳しく説明する価値があります。この動作はに固有のものではありませんawait using。プレーンusingブロックでも発生します。だから私がDispose()ここで言う間、それはすべてにDisposeAsync()もあてはまります。

usingブロックがためだけの構文糖であるtry/のfinallyように、ブロックの文書の発言部分を言います。例外が発生した後でも、finallyブロックは常に実行されるため、表示されるのはそのとおりです。したがって、例外が発生し、catchブロックがない場合は、finallyブロックが実行されるまで例外が保留され、その後例外がスローされます。ただし、例外がで発生した場合finally、古い例外は表示されません。

この例でこれを確認できます。

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

内で呼び出されるかどうDispose()DisposeAsync()は関係ありませんfinally。動作は同じです。

私の最初の考えは次のとおりですDispose()。しかし、Microsoft自身のコードのいくつかを確認した後、それは場合によって異なると思います。

FileStreamたとえば、の実装を見てください。両方とも同期Dispose()メソッドであり、DisposeAsync()実際には例外をスローできます。同期Dispose()一部の例外を意図的に無視しますが、すべてを無視するわけではありません。

ただし、クラスの性質を考慮に入れることが重要だと思います。ではFileStream、例えば、Dispose()ファイルシステムにバッファをフラッシュします。これは非常に重要なタスクであり、それが失敗したかどうかを知る必要があります。あなたはそれを無視することはできません。

ただし、他のタイプのオブジェクトでは、を呼び出すとDispose()、そのオブジェクトをまったく使用できなくなります。呼び出すDispose()というのは、「このオブジェクトは私には死んでいる」という意味です。多分それは割り当てられたメモリをクリーンアップしますが、失敗してもアプリケーションの動作には影響しません。その場合、内の例外を無視することを決定するかもしれませんDispose()

ただし、いずれの場合でも、内のusing例外またはからの例外を区別したい場合は、ブロックの内側と外側の両方に/ ブロックDispose()が必要です。trycatchusing

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
}

または、単に使用できませんでしたusingtry/ catch/ finallyブロックを自分で書き、そこで例外をキャッチします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
ところで、source.dot.net(.NET Core)/ referencesource.microsoft.com(.NET Framework)は、GitHubよりもはるかに簡単に閲覧できます
canton7

回答ありがとうございます!私は本当の理由が何であるかを知っています(私は質問でtry / finallyと同期のケースについて述べました)。今あなたの提案について。通常、例外処理はブロック自体から離れた場所で行われるため、ブロックのcatch 内部using役に立ちませんusing。したがって、内部usingでの処理は通常、あまり実用的ではありません。noの使用についてusing—提案された回避策より本当に良いですか?
ヴラド

2
@ canton7すごい!私は知っていたreferencesource.microsoft.comが、.NETのコアの等価があった知りませんでした。ありがとう!
Gabriel Luci

@Vlad "Better"は、あなただけが答えられるものです。私は他の誰かのコードを読んでいたならば、私が見て希望を知っているtry/ catch/ finallyそれはすぐにもそれが何を読んで移動することなくやっているものをクリアしますので、ブロックをAsyncUsingやっています。あなたはまた、早期復帰を行うオプションを保持します。また、追加のCPUコストが発生しますAwaitUsing。小さいですが、あります。
Gabriel Luci

2
@PauloMorgadoこれは、複数回呼び出されるためDispose()スローすべきではないことを意味します。この回答で示したように、Microsoft独自の実装では例外がスローされる可能性があります。ただし、通常はスローすることを誰も期待しないので、可能な限り回避することに同意します。
Gabriel Luci

4

usingは事実上例外処理コードです(try ... finally ... Dispose()の構文砂糖)。

例外処理コードが例外をスローしている場合、何かが王室で破壊されています。

他に何が起こってもあなたをそこに連れて行ってさえ、もはやもう関係はありません。欠陥のある例外処理コードは、考えられるすべての例外を何らかの方法で隠します。例外処理コードは修正する必要があります。これは絶対的な優先順位を持っています。それがないと、実際の問題に十分なデバッグデータを取得できません。私はそれがひどく間違っているのをよく見ます。裸のポインタを処理するのと同じくらい簡単に間違えやすい。多くの場合、私がリンクしている主題に関する2つの記事があり、それらは根本的な設計の誤解を助けるかもしれません:

例外の分類に応じて、例外処理/ポーズコードが例外をスローした場合、次のことを行う必要があります。

Fatal、Boneheaded、Vexingのソリューションは同じです。

外因性の例外は、重大なコストをかけても回避する必要があります。例外をログに記録するために、ログデータベースではなくログファイルを使用するのには理由があります。DBオペレーションは、外因性の問題が発生しやすい方法です。ログファイルは1つのケースであり、ファイルハンドルをランタイム全体で開いたままにしておいてもかまいません。

接続を閉じる必要がある場合は、相手側をあまり気にする必要はありません。UDPのように処理します。「情報を送信しますが、反対側がそれを取得するかどうかは気にしません。」破棄とは、作業中のクライアント側/側のリソースをクリーンアップすることです。

通知してみます。しかし、サーバー/ FS側のものをクリーンアップしますか?それは何である彼らのタイムアウトとその例外処理が担当します。


つまり、あなたの提案は結局、接続を閉じる際の例外を抑制することに要約されますよね?
Vlad

@Vlad外因性のもの?承知しました。Dipose / Finalizerは、自分自身でクリーンアップするためのものです。例外のためにConnecitonインスタンスを閉じる可能性があります。とにかく、接続機能していないためです。そして、以前の「接続なし」例外を処理しているときに「接続なし」例外を取得することにはどのような点がありますか 単一の「Yo、私はこの接続を閉じています」を送信します。この場合、すべての外因性例外を無視するか、ターゲットに近づいても無視します。AfaikのDisposeのデフォルトの実装はすでにそれを行っています。
Christopher

@Vlad:覚えておきますが、例外をスローすることは決してないはずのもの(コーラスFatalのものを除く)はたくさんあります。タイプInitliaizersはリストの上位にあります。Disposeもその1つです。「リソースが常に適切にクリーンアップされるようにするには、例外をスローせずにDisposeメソッドを複数回呼び出す必要があります。」docs.microsoft.com/en-us/dotnet/standard/garbage-collection/...
クリストファー

@Vlad致命的な例外のチャンス?私たちはいつもそれらを危険にさらす必要があり、「コールDispose」を超えてそれらを処理してはなりません。そして、実際には何もすべきではありません。実際には、ドキュメントには何も記載されていません。| 骨の折れる例外?常に修正してください。| Vexing例外は、TryParse()のように、嚥下/処理の主要な候補です。外因性?また、常にハンドヘルドする必要があります。多くの場合、それらをユーザーに通知してログに記録することもできます。しかし、それ以外の場合は、プロセスを強制終了する価値はありません。
Christopher

@Vlad SqlConnection.Dispose()を検索しました。それもしない気に接続が終わったことについてサーバには何も送信します。NativeMethods.UnmapViewOfFile();およびの結果、まだ何かが発生する可能性がありNativeMethods.CloseHandle()ます。しかし、それらは外部からインポートされます。これらの2つが遭遇する可能性のあるものについて、適切な.NET例外を取得するために使用できる戻り値やその他のチェックはありません。だから私は強く推測している、SqlConnection.Dispose(bool)は単に気にしない。| Closeの方がはるかに優れており、実際にサーバーに通知します。disposeを呼び出す前。
Christopher

1

AggregateExceptionを使用して、次のようにコードを変更できます。

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.