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예외가 발생하고 예외가 await using발생 AsyncDispose하지 않는 경우에만 내부에서 예외가 발생 합니다 .

그러나 await using가능하면 블록 에서 예외를 가져오고 블록이 성공적으로 완료 된 DisposeAsync경우에만 -exception을 사용 하는 것이 좋습니다 await using.

이론적 근거 : 수업 D에서 일부 네트워크 리소스를 사용하고 일부 알림을 원격으로 구독 한다고 가정합니다 . 내부 코드 await using가 잘못하여 통신 채널에 장애가 생길 수 있습니다. 그 후 Dispose의 코드가 정상적으로 통신을 종료하려고 시도하면 (예 : 알림 수신 거부) 코드가 실패합니다. 그러나 첫 번째 예외는 문제에 대한 실제 정보를 제공하고 두 번째 예외는 단지 두 번째 문제입니다.

다른 경우에는 주요 부분이 통과되어 폐기가 실패한 경우 실제 문제는 내부 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

많은 코딩 표준이 초기 반품을 금지하는 이유가 있습니다. :) 네트워킹이 관련된 곳에서는 약간의 명시 적 표현이 나쁜 것은 아닙니다. Dispose"일이 잘못되었을 수도 있습니다. 상황을 개선하기 위해 최선을 다하지만 상황을 악화시키지 마십시오."라는 이유 AsyncDispose는 무엇인지 알 수 없습니다.
canton7

@ canton7 : 글쎄요, 예외가있는 언어에서는 모든 문장이 일찍 돌아올 수도 있습니다 :-\
Vlad

그렇습니다. 그러나 예외적 일 것 입니다. 이 경우 DisposeAsync정리하기 위해 최선을 다 하지만 던지지 않는 것이 옳은 일입니다. 당신은 의도적 인 조기 수익 에 대해 이야기하고있었습니다 . 의도적 인 조기 수익은 실수로 전화를 우회 할 수 있습니다 CloseAsync.
canton7

답변:


3

처리하려는 예외 (현재 요청을 방해하거나 프로세스를 중단)가있을 수 있으며 설계에서 때때로 발생할 것으로 예상되는 예외가 있으며 처리 할 수있는 예외 (예 : 재시도 및 계속)가 있습니다.

그러나이 두 가지 유형을 구별하는 것은 코드의 궁극적 인 호출자에게 달려 있습니다. 이는 결정을 호출자에게 맡기는 예외의 핵심입니다.

때때로 호출자는 원래 코드 블록에서 예외를 표면화하고 때로는에서 예외를 표면 처리하는 데 우선 순위를 둡니다 Dispose. 우선 순위를 결정해야하는 일반적인 규칙은 없습니다. CLR은 동기화 동작과 비 동기화 동작간에 적어도 일관 적입니다.

불행히도 이제 우리는 AggregateException여러 예외를 나타내야하지만, 이것을 해결하기 위해 개조 할 수는 없습니다. 즉, 예외가 이미 비행 중이고 다른 예외가 발생한 경우에는로 결합됩니다 AggregateException. 이 catch메커니즘을 수정하여 쓰면 type 예외를 포함하는 것을 catch (MyException)잡을 수 있습니다. 이 아이디어에서 비롯된 여러 가지 다른 합병증이 있으며, 지금 너무 근본적인 것을 수정하는 것은 너무 위험합니다.AggregateExceptionMyException

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맹목적으로 모든 것을 잡을 수는 없지만 치명적이지 않은 것 ( Eric Lippert의 사용법 에서 골머리가 아님) 만 잡도록 수정해야합니다 .)
Vlad

@Vlad 예-올바른 접근 방식은 컨텍스트에 전적으로 의존합니다. 또한 예외 유형의 전역 분류를 사용하여 예외를 포착해야하는지 여부에 따라 UsingAsync를 한 번 작성할 수는 없습니다. 다시 이것은 상황에 따라 다르게 결정되는 결정입니다. Eric Lippert가 이러한 범주에 대해 언급 할 때 예외 유형에 대한 본질적인 사실은 아닙니다. 예외 유형별 범주는 디자인에 따라 다릅니다. 때때로 의도적으로 IOException이 예상되는 경우도 있습니다.
Daniel Earwicker

4

어쩌면 이미 이런 일이 발생했는지 이해할 수도 있지만 철자가 가치가 있습니다. 이 동작은 특정되지 않습니다 await using. 일반 using블록에서도 발생합니다. 제가 Dispose()여기서 말하는 동안 , 그것은 모두 적용됩니다 DisposeAsync().

using블록은 단지 구문 설탕입니다try /의 finally는 AS, 블록 문서의 발언 섹션 말한다. 예외 후에도 항상finally 블록이 실행 되기 때문에 발생 하는 결과입니다. 따라서 예외가 발생하고 블록 이없는 경우 블록이 실행될 때까지 예외가 보류 된 다음 예외가 발생합니다. 그러나에서 예외가 발생 하면 이전 예외가 표시되지 않습니다.catchfinallyfinally

이 예제를 통해이를 확인할 수 있습니다.

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

내부에서 호출 되는지 Dispose()또는 DisposeAsync()내부에서 호출 되는지 는 중요하지 않습니다 finally. 동작은 같습니다.

내 첫 번째 생각은 : 던지지 마십시오 Dispose(). 그러나 Microsoft 자체 코드 중 일부를 검토 한 후에는 코드에 따라 다릅니다.

FileStream예를 들어 구현을 살펴보십시오 . 동기식 Dispose()DisposeAsync() 실제로 예외를 던질 수 있습니다. 동기 Dispose()일부 예외를 의도적으로 무시 하지만 전부는 아닙니다.

하지만 수업의 본질을 고려하는 것이 중요하다고 생각합니다. (A)에 FileStream, 예를 들어, Dispose()파일 시스템의 버퍼를 플러시한다. 이것은 매우 중요한 작업이며 실패한 경우 알아야합니다 . 당신은 그것을 무시할 수 없습니다.

그러나 다른 유형의 객체 Dispose()에서는를 호출 하면 더 이상 객체를 사용할 필요가 없습니다. Dispose()실제로 전화 한다는 것은 "이 개체가 나에게 죽었다"는 것을 의미합니다. 어쩌면 할당 된 메모리를 정리하지만 실패해도 응용 프로그램 작동에 영향을 미치지 않습니다. 이 경우의 내부 예외를 무시하기로 결정할 수 있습니다 Dispose().

그러나 어쨌든 내부 using또는 외부의 예외를 구별 Dispose()하려면 블록 내부 및 외부 에서 try/ catch블록 이 필요합니다 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
}

아니면 그냥 사용할 수 없습니다 using. 다음 에서 예외를 발견 할 수 있는 try/ 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
Btw, source.dot.net (.NET Core) / referencesource.microsoft.com (.NET Framework)은 GitHub보다 탐색하기가 훨씬 쉽습니다
canton7

답변 주셔서 감사합니다! 나는 실제 이유가 무엇인지 알고 있습니다 (질문에서 try / finally 및 동기식 사례를 언급했습니다). 이제 제안에 대해 A는 catch 내부using 블록 것 일반적으로 예외 처리가 어딘가 멀리에서 수행되므로하지 도움이 using때문에 내부를 처리, 블록 자체는 using일반적으로 매우 실용적되지 않습니다. 아니요를 사용하는 방법 using— 제안 된 해결 방법보다 실제로 더 낫습니까?
Vlad

2
트윗 담아 가기 referencesource.microsoft.com 에 대해 알고 있었지만 .NET Core에 해당하는 것이 있는지 몰랐습니다. 감사!
Gabriel Luci

@ 블라드 "더 나은"은 당신 만이 대답 할 수있는 것입니다. 나는 다른 사람의 코드를 읽는 경우, 내가보고 선호 알고 try/ catch/ finally즉시 일이 무엇인지 읽어 갈 필요없이 무엇을하고 있는지 취소 것이기 때문에 블록을 AsyncUsing하고있다. 또한 조기 반품 옵션을 유지합니다. 또한 추가 CPU 비용이 발생합니다 AwaitUsing. 작지만 거기에 있습니다.
가브리엘 루시

2
@PauloMorgado 그것은 단지 두 번 이상 호출 되기 때문에Dispose() 던져서 는 안된다는 것을 의미합니다 . 이 답변에 표시된 것처럼 Microsoft 자체 구현에서는 예외가 발생할 수 있습니다. 그러나 나는 아무도 그것을 일반적으로 던질 것으로 예상하지 않기 때문에 가능한 한 피하지 않으면 안된다는 데 동의합니다.
Gabriel Luci

4

using은 효과적으로 예외 처리 코드입니다. try ... finally ... Dispose ()의 구문 설탕.

예외 처리 코드에서 예외가 발생하면 문제가 발생합니다.

당신을 거기에 데려다 줬던 일이 더 이상 더 이상 교전하지 않습니다. 잘못된 예외 처리 코드는 가능한 모든 예외를 숨길 수 있습니다. 예외 처리 코드는 반드시 수정되어야하며 절대 우선 순위를 갖습니다. 그렇지 않으면 실제 문제에 대한 디버깅 데이터가 충분하지 않습니다. 나는 그것이 종종 잘못되었다고 본다. 알몸의 포인터를 다루는 것처럼 잘못되기 쉽습니다. 종종 주제 I 링크에 대한 두 가지 기사가 있습니다.

Exception 분류에 따라 Exception Handling / Dipose 코드에서 Exception이 발생하면 다음을 수행해야합니다.

치명적, 본 헤드 및 벡싱의 경우 솔루션은 동일합니다.

외인성 예외는 심각한 비용으로도 피해야합니다. 예외를 기록하기 위해 로그 데이터베이스 대신 로그 파일을 사용하는 이유가 있습니다. DB Opeartions는 외인성 문제가 발생하기 쉽습니다. 로그 파일은 파일 핸들을 유지하더라도 신경 쓰지 않는 경우입니다. 런타임 전체를여십시오.

연결을 끊어야하는 경우 다른 쪽 끝을 걱정하지 않아도됩니다. UDP와 같이 처리하십시오. "정보를 보내 겠지만 상대방이 정보를 얻는 지 상관하지 않습니다." 폐기는 작업중인 클라이언트 / 측의 리소스를 정리하는 것입니다.

그들에게 알리려고 노력할 수 있습니다. 그러나 서버 / FS 쪽에서 물건을 정리합니까? 즉 무엇인가 자신의 시간 제한과 예외 처리에 대한 책임이 있습니다.


따라서 귀하의 제안은 연결 종료에 대한 예외를 효과적으로 억제합니다.
Vlad

@Vlad 외생적인 것? 확실한. Dipose / Finalizer는 자체적으로 청소할 수 있습니다. 예외로 인해 Conneciton 인스턴스를 닫을 때 기회가 더 이상 연결되어 있지 않을 수 있습니다 . 그리고 이전 "연결 없음"예외를 처리하는 동안 "연결 없음"예외가 발생하는 시점은 무엇입니까? 외인성 예외를 모두 무시하거나 대상에 가까워 지더라도 단일 "Yo,이 연결을 닫는 중"을 보냅니다. Apose는 Dispose의 기본 구현이 이미 수행합니다.
Christopher

@ 블라드 : 나는 당신이 예외를 던져서는 안되는 것들이 많다는 것을 기억합니다 (치명적인 것들 제외). Type Initliaizer가 목록에 있습니다. Dispose는 다음 중 하나입니다. "자원을 항상 적절하게 정리할 수 있도록 예외를 발생시키지 않고 Dispose 메서드를 여러 번 호출 할 수 있어야합니다." docs.microsoft.com/ko-kr/dotnet/standard/garbage-collection/…
Christopher

@Vlad 치명적인 예외의 기회? 우리는 이러한 위험을 감수해야하며 "호출 처리"이상으로 처리해서는 안됩니다. 그리고 그들과는 아무런 관계가 없어야합니다. 그들은 실제로 어떤 문서에도 언급되지 않았습니다. | 골머리 예외? 항상 수정하십시오. | Vexing 예외는 TryParse () | 에서처럼 삼키기 / 처리의 주요 후보입니다. 외인성? 또한 항상 다루어야합니다. 종종 사용자에게 그들에 대해 알리고 기록하기를 원합니다. 그러나 그렇지 않으면 프로세스를 죽일 가치가 없습니다.
Christopher

@ Vlad SqlConnection.Dispose ()를 찾았습니다. 심지어하지 않는 신경 연결이 끝난 것에 대해 서버에 아무것도를 보낼 수 있습니다. NativeMethods.UnmapViewOfFile();및의 결과로 여전히 문제가 발생할 수 있습니다 NativeMethods.CloseHandle(). 그러나 그것들은 extern에서 수입됩니다. 반환 값이나 그 두 가지가 발생할 수있는 주위에 적절한 .NET 예외를 얻는 데 사용될 수있는 다른 값은 확인하지 않습니다. 따라서 SqlConnection.Dispose (bool)은 단순히 신경 쓰지 않는다고 강력하게 가정합니다. | 닫기는 실제로 서버에 알리는 것이 훨씬 좋습니다. 호출하기 전에 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.