[c#] AsyncDispose에서 예외를 처리하는 올바른 방법

새로운 .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있습니까? 인터넷에서 검색해도이 문제에 대해 논의하지 못했습니다.



답변

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

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

때때로 호출자는 원래 코드 블록에서 예외를 표면화하고 때로는에서 예외를 표면 처리하는 데 우선 순위를 둡니다 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. 일반 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
    }
}


답변

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

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

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

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

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

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

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

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


답변

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


답변