비동기 void 메소드에 의해 발생 된 예외를 잡아라


283

Microsoft의 .NET 용 비동기 CTP를 사용하면 호출 방법에서 비동기 방법으로 발생한 예외를 포착 할 수 있습니까?

public async void Foo()
{
    var x = await DoSomethingAsync();

    /* Handle the result, but sometimes an exception might be thrown.
       For example, DoSomethingAsync gets data from the network
       and the data is invalid... a ProtocolException might be thrown. */
}

public void DoFoo()
{
    try
    {
        Foo();
    }
    catch (ProtocolException ex)
    {
          /* The exception will never be caught.
             Instead when in debug mode, VS2010 will warn and continue.
             The deployed the app will simply crash. */
    }
}

따라서 기본적으로 비동기 코드의 예외가 가능하다면 호출 코드로 버블 링되기를 원합니다.



22
미래에 누군가이 문제에 걸려 넘어 질 경우 Async / Await Best Practices ... 기사 에 "그림 2 비동기 무효화 방법의 예외를 잡아낼 수 없음"에 대한 설명이 있습니다. " 비동기 Task 또는 비동기 Task <T> 메소드에서 예외가 발생하면 해당 예외가 캡처되어 Task 오브젝트에 배치됩니다. async void 메소드에는 Task 오브젝트가 없으며 비동기 void 메소드에서 예외가 발생합니다. 비동기 void 메소드가 시작될 때 활성화 된 SynchronizationContext에서 직접 발생합니다. "
Mr Moose

당신이 사용할 수있는 이 방법 또는
Tselofan

답변:


263

읽는 것이 다소 이상하지만 그렇습니다. 예외는 호출 코드까지 발생하지만 귀하 await또는 Wait()호출하는 경우Foo 에만 가능합니다 .

public async Task Foo()
{
    var x = await DoSomethingAsync();
}

public async void DoFoo()
{
    try
    {
        await Foo();
    }
    catch (ProtocolException ex)
    {
          // The exception will be caught because you've awaited
          // the call in an async method.
    }
}

//or//

public void DoFoo()
{
    try
    {
        Foo().Wait();
    }
    catch (ProtocolException ex)
    {
          /* The exception will be caught because you've
             waited for the completion of the call. */
    }
} 

비동기 void 메소드에는 서로 다른 오류 처리 의미가 있습니다. 비동기 작업 또는 비동기 작업 메서드에서 예외가 발생하면 해당 예외가 캡처되어 Task 개체에 배치됩니다. 비동기 void 메소드에는 Task 객체가 없으므로 비동기 void 메소드에서 발생하는 예외는 비동기 void 메소드가 시작될 때 활성화 된 SynchronizationContext에서 직접 발생합니다. -https : //msdn.microsoft.com/en-us/magazine/jj991977.aspx

.Net이 메소드를 동기식으로 실행하기로 결정한 경우 Wait ()를 사용하면 애플리케이션이 차단 될 수 있습니다.

이 설명 http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions 는 꽤 좋습니다. 컴파일러가이 마법을 달성하기 위해 취하는 단계에 대해 설명합니다.


3
나는 실제로 읽는 것이 간단하다는 것을 의미합니다. 반면에 실제로 일어나고있는 일이 정말 복잡하다는 것을 알고 있습니다. 그래서 제 뇌는 내 눈을 믿지
Stuart

8
Foo () 메소드는 void 대신 Task로 표시되어야한다고 생각합니다.
Sornii

4
이것이 이것이 AggregateException을 생성 할 것이라고 확신합니다. 따라서이 답변에 표시된 catch 블록은 예외를 catch하지 않습니다.
xanadont

2
"그러나 Foo에 대한 호출을 기다리거나 Wait () 한 경우에만" awaitFoo가 void를 반환 할 때 Foo에 대한 호출을 어떻게 할 수 있습니까? async void Foo(). Type void is not awaitable?
rism

3
void 메소드를 기다릴 수 없습니까?
Hitesh P

74

예외가 포착되지 않는 이유는 Foo () 메소드에 void 리턴 유형이 있으므로 await가 호출되면 단순히 리턴하기 때문입니다. DoFoo ()가 Foo의 완료를 기다리지 않으므로 예외 핸들러를 사용할 수 없습니다.

메소드 서명을 변경할 수 있으면 더 간단한 솔루션이 열립니다- Foo()유형을 반환하도록 변경 Task한 다음DoFoo()await Foo(),이 코드에서와 같이 :

public async Task Foo() {
    var x = await DoSomethingThatThrows();
}

public async void DoFoo() {
    try {
        await Foo();
    } catch (ProtocolException ex) {
        // This will catch exceptions from DoSomethingThatThrows
    }
}

19
이것은 실제로 당신에게 몰래 들어갈 수 있으며 컴파일러에 의해 경고되어야합니다.
GGleGrand

19

당신의 코드는 당신이 생각하는 것을하지 않습니다. 비동기 메서드는 메서드가 비동기 결과를 기다리는 즉시 시작합니다. 코드가 실제로 어떻게 작동하는지 조사하기 위해 추적을 사용하는 것이 통찰력이 있습니다.

아래 코드는 다음을 수행합니다.

  • 4 가지 작업 생성
  • 각 작업은 비동기 적으로 숫자를 증가시키고 증가 된 숫자를 반환합니다
  • 비동기 결과가 도착하면 추적됩니다.

 

static TypeHashes _type = new TypeHashes(typeof(Program));        
private void Run()
{
    TracerConfig.Reset("debugoutput");

    using (Tracer t = new Tracer(_type, "Run"))
    {
        for (int i = 0; i < 4; i++)
        {
            DoSomeThingAsync(i);
        }
    }
    Application.Run();  // Start window message pump to prevent termination
}


private async void DoSomeThingAsync(int i)
{
    using (Tracer t = new Tracer(_type, "DoSomeThingAsync"))
    {
        t.Info("Hi in DoSomething {0}",i);
        try
        {
            int result = await Calculate(i);
            t.Info("Got async result: {0}", result);
        }
        catch (ArgumentException ex)
        {
            t.Error("Got argument exception: {0}", ex);
        }
    }
}

Task<int> Calculate(int i)
{
    var t = new Task<int>(() =>
    {
        using (Tracer t2 = new Tracer(_type, "Calculate"))
        {
            if( i % 2 == 0 )
                throw new ArgumentException(String.Format("Even argument {0}", i));
            return i++;
        }
    });
    t.Start();
    return t;
}

흔적을 볼 때

22:25:12.649  02172/02820 {          AsyncTest.Program.Run 
22:25:12.656  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.657  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 0    
22:25:12.658  02172/05220 {          AsyncTest.Program.Calculate    
22:25:12.659  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.659  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 1    
22:25:12.660  02172/02756 {          AsyncTest.Program.Calculate    
22:25:12.662  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.662  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 2    
22:25:12.662  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.662  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 3    
22:25:12.664  02172/02756          } AsyncTest.Program.Calculate Duration 4ms   
22:25:12.666  02172/02820          } AsyncTest.Program.Run Duration 17ms  ---- Run has completed. The async methods are now scheduled on different threads. 
22:25:12.667  02172/02756 Information AsyncTest.Program.DoSomeThingAsync Got async result: 1    
22:25:12.667  02172/02756          } AsyncTest.Program.DoSomeThingAsync Duration 8ms    
22:25:12.667  02172/02756 {          AsyncTest.Program.Calculate    
22:25:12.665  02172/05220 Exception   AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 0   
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     
22:25:12.668  02172/02756 Exception   AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 2   
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     
22:25:12.724  02172/05220          } AsyncTest.Program.Calculate Duration 66ms      
22:25:12.724  02172/02756          } AsyncTest.Program.Calculate Duration 57ms      
22:25:12.725  02172/05220 Error       AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 0  

Server stack trace:     
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     

Exception rethrown at [0]:      
   at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()    
   at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()  
   at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 106    
22:25:12.725  02172/02756 Error       AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 2  

Server stack trace:     
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     

Exception rethrown at [0]:      
   at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()    
   at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()  
   at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 0      
22:25:12.726  02172/05220          } AsyncTest.Program.DoSomeThingAsync Duration 70ms   
22:25:12.726  02172/02756          } AsyncTest.Program.DoSomeThingAsync Duration 64ms   
22:25:12.726  02172/05220 {          AsyncTest.Program.Calculate    
22:25:12.726  02172/05220          } AsyncTest.Program.Calculate Duration 0ms   
22:25:12.726  02172/05220 Information AsyncTest.Program.DoSomeThingAsync Got async result: 3    
22:25:12.726  02172/05220          } AsyncTest.Program.DoSomeThingAsync Duration 64ms   

Run 메소드는 스레드 2820에서 완료되고 하나의 하위 스레드 만 완료됨을 알 수 있습니다 (2756). await 메소드 주위에 try / catch를 넣으면 계산 작업이 완료되고 연속성이 실행될 때 다른 스레드에서 코드가 실행되지만 일반적인 방식으로 예외를 "catch"할 수 있습니다.

계산 방법은 ApiChange 도구 에서 ApiChange.Api.dll을 사용했기 때문에 throw 된 예외를 자동으로 추적합니다 . Tracing and Reflector는 무슨 일이 일어나고 있는지 이해하는 데 도움이됩니다. 스레딩을 없애기 위해 고유 한 버전의 GetAwaiter BeginAwait 및 EndAwait를 생성하고 작업을 래핑하지 않고 자신의 확장 메서드 내에서 Lazy 및 추적을 수행 할 수 있습니다. 그러면 컴파일러와 TPL의 기능을 훨씬 잘 이해할 수 있습니다.

이제 예외를 전파 할 스택 프레임이 없기 때문에 예외를 다시 시도하거나 잡아낼 수있는 방법이 없음을 알 수 있습니다. 비동기 작업을 시작한 후 코드가 완전히 다른 작업을 수행 할 수 있습니다. Thread.Sleep을 호출하거나 종료 할 수도 있습니다. 하나의 포 그라운드 스레드가 남아있는 한 응용 프로그램은 계속해서 비동기 작업을 계속 실행합니다.


비동기 작업이 완료되고 UI 스레드를 다시 호출 한 후 비동기 메서드 내에서 예외를 처리 할 수 ​​있습니다. 권장되는 방법은 TaskScheduler.FromSynchronizationContext 입니다. UI 스레드가 있고 다른 것들로 바쁘지 않은 경우에만 작동합니다.


5

비동기 함수에서 예외가 발생할 수 있습니다.

public async void Foo()
{
    try
    {
        var x = await DoSomethingAsync();
        /* Handle the result, but sometimes an exception might be thrown
           For example, DoSomethingAsync get's data from the network
           and the data is invalid... a ProtocolException might be thrown */
    }
    catch (ProtocolException ex)
    {
          /* The exception will be caught here */
    }
}

public void DoFoo()
{
    Foo();
}

2
저도 알아요.하지만 UI에 정보를 표시하려면 DoFoo에 해당 정보가 필요합니다. 이 경우 최종 사용자 도구가 아니라 통신 프로토콜을 디버깅하는 도구 인 UI를 예외로 표시하는 것이 중요합니다.
TimothyP

이 경우, 콜백은 많은 의미합니다 (좋은 오래된 비동기 대표).
Sanjeevakumar Hiremath

@Tim : 발생 된 예외에 필요한 정보를 포함 하시겠습니까?
Eric J.

5

비동기 메소드에 void 리턴 유형이있는 경우 예외의 시간순 스택 추적이 유실됩니다. 다음과 같이 작업을 반환하는 것이 좋습니다. 디버깅이 훨씬 쉬워집니다.

public async Task DoFoo()
    {
        try
        {
            return await Foo();
        }
        catch (ProtocolException ex)
        {
            /* Exception with chronological stack trace */     
        }
    }

시도가있는 동안 예외가 없으면 값이 반환되지 않기 때문에 값을 반환하는 모든 경로에 문제가 발생하지 않습니다. return그러나 명령문 이 없으면 이 코드는 Task을 사용하여 "암시 적으로"리턴 되므로 작동합니다 async / await.
Matias Grioni 2019

2

이 블로그는 Async Best Practices 문제를 깔끔하게 설명합니다 .

비동기 이벤트 핸들러가 아닌 한 비동기 메소드의 리턴으로 void를 사용해서는 안됩니다. 예외를 잡을 수 없기 때문에 이것은 나쁜 습관입니다. ;-).

가장 좋은 방법은 반환 유형을 작업으로 변경하는 것입니다. 또한 모든 방식으로 비동기 코드를 작성하고 모든 비동기 메소드를 호출하고 비동기 메소드에서 호출하십시오. 콘솔에서 Main 메서드를 제외하고 비동기식으로 사용할 수 없습니다 (C # 7.1 이전).

이 모범 사례를 무시하면 GUI 및 ASP.NET 응용 프로그램에서 교착 상태가 발생합니다. 교착 상태는 이러한 응용 프로그램이 하나의 스레드 만 허용하는 컨텍스트에서 실행되고 비동기 스레드로 양도하지 않기 때문에 발생합니다. 이것은 GUI가 동 기적으로 리턴을 기다리는 반면 async 메소드는 컨텍스트 : 교착 상태를 기다리는 것을 의미합니다.

이 문제는 콘솔 풀에서 컨텍스트에서 실행되기 때문에 콘솔 응용 프로그램에서는 발생하지 않습니다. 비동기 메서드는 예약 될 다른 스레드에서 반환됩니다. 이것이 테스트 콘솔 앱이 작동하는 이유이지만 다른 애플리케이션에서는 동일한 호출이 교착 상태에 빠질 것입니다 ...


1
"콘솔에서 Main 메소드를 제외하고는 비동기식 일 수 없습니다." C # 7.1부터 Main은 이제 비동기 메소드 링크가
Adam
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.