다음을 통해 공유


비동기 애플리케이션 디버그

이 자습서에서는 병렬 스택 창의 작업 보기를 사용하여 C# 비동기 애플리케이션을 디버그하는 방법을 보여 줍니다. 이 창은 TAP(작업 기반 비동기 패턴)라고도 하는 비동기/대기 패턴을 사용하는 코드의 런타임 동작을 이해하고 확인하는 데 도움이 됩니다.

비동기/대기 패턴이 아닌 TPL(작업 병렬 라이브러리)을 사용하는 앱 또는 동시성 런타임을 사용하는 C++ 앱의 경우 병렬 스택 창의 스레드 보기가 디버깅에 가장 유용한 도구입니다. 자세한 내용은 병렬 스택 창에서 스레드 및 작업 보기를 참조하세요.

작업 보기는 다음을 수행하는 데 도움이 됩니다.

  • 비동기/대기 패턴을 사용하는 앱에 대한 호출 스택 시각화를 봅니다. 이러한 시나리오에서 작업 보기는 앱 상태에 대한 보다 완전한 그림을 제공합니다.

  • 실행되도록 예약되었지만 아직 실행되지 않는 비동기 코드를 식별합니다. 예를 들어 데이터를 반환하지 않은 HTTP 요청은 스레드 보기 대신 작업 보기에 표시되어 문제를 격리하는 데 도움이 됩니다.

  • 차단되거나 대기 중인 작업과 같은 잠재적 문제와 관련된 힌트와 함께 동기화 오버 비동기 패턴과 같은 문제를 식별하는 데 도움이 됩니다. 비동기 위의 동기 코드 패턴은 동기 방식으로 비동기 메서드를 호출하는 코드를 나타내며, 이는 스레드를 차단한다는 것이 알려져 있어 스레드 풀 부족의 가장 일반적인 원인입니다.

C# 샘플

이 가이드의 샘플 코드는 고릴라의 일상 중 하루를 시뮬레이션하는 애플리케이션에 대한 것입니다. 연습의 목적은 병렬 스택 창의 작업 보기를 사용하여 비동기 애플리케이션을 디버그하는 방법을 이해하는 것입니다.

이 샘플에는 스레드 풀 고갈을 초래할 수 있는 비동기 위에 동기화 안티패턴을 사용하는 예제가 포함되어 있습니다.

호출 스택을 직관적으로 만들기 위해 샘플 앱은 다음 순차적 단계를 수행합니다.

  1. 고릴라를 나타내는 개체를 만듭니다.
  2. 고릴라가 깨어난다.
  3. 고릴라는 아침 산책을 간다.
  4. 고릴라는 정글에서 바나나를 발견한다.
  5. 고릴라가 먹는다.
  6. 고릴라는 원숭이 사업에 종사한다.

비동기 호출 스택

병렬 스택의 작업 보기는 비동기 호출 스택에 대한 시각화를 제공하므로 애플리케이션에서 어떤 일이 일어나고 있는지(또는 발생할 예정)를 확인할 수 있습니다.

다음은 작업 보기에서 데이터를 해석할 때 기억해야 할 몇 가지 중요한 사항입니다.

  • 비동기 호출 스택은 스택을 나타내는 실제 호출 스택이 아니라 논리 또는 가상 호출 스택입니다. 비동기 코드(예: 키워드 사용)를 사용하는 await 경우 디버거는 "비동기 호출 스택" 또는 "가상 호출 스택"의 보기를 제공합니다. 비동기 호출 스택은 스레드 기반 호출 스택 또는 "물리적 스택"과 다릅니다. 왜냐하면 비동기 호출 스택은 현재 실제 스레드에서 실행 중일 필요가 없기 때문입니다. 대신 비동기 호출 스택은 비동기적으로 향후 실행될 코드의 계승 또는 "프라미스"입니다. 호출 스택은 연속을 사용하여 만들어집니다.

  • 예약되었지만 현재 실행되고 있지 않은 비동기 코드는 실제 호출 스택에 표시되지 않지만 작업 보기의 비동기 호출 스택에 표시됩니다. .Wait 또는 .Result 메서드를 사용하여 스레드를 차단하는 경우, 물리적 호출 스택에 코드가 표시될 수 있습니다.

  • 비동기 가상 호출 스택은 메서드 호출(예 .WaitAny : 또는 .WaitAll)의 사용으로 인한 분기로 인해 항상 직관적인 것은 아닙니다.

  • 호출 스택 창은 현재 실행 중인 스레드에 대한 실제 호출 스택을 표시하므로 작업 보기와 함께 유용할 수 있습니다.

  • 가상 호출 스택의 동일한 섹션은 복잡한 앱에 대한 시각화를 간소화하기 위해 함께 그룹화됩니다.

    다음 개념 애니메이션은 그룹화가 가상 호출 스택에 적용되는 방법을 보여 줍니다. 가상 호출 스택의 동일한 세그먼트만 그룹화됩니다.

    가상 호출 스택의 그룹화 그림입니다.

샘플 프로젝트 만들기

  1. Visual Studio를 열고 새 프로젝트를 만듭니다.

    시작 창이 열려 있지 않으면 파일>시작 창선택합니다.

    시작 창에서 새 프로젝트를 선택합니다.

    새 프로젝트 만들기 창에서 검색 상자에 콘솔을 입력하거나 입력합니다. 그런 다음 언어 목록에서 C# 을 선택한 다음 플랫폼 목록에서 Windows 를 선택합니다.

    언어 및 플랫폼 필터를 적용한 후 .NET용 콘솔 앱을 선택한 다음, 다음을 선택합니다.

    비고

    올바른 템플릿이 표시되지 않는 경우 도구>도구 및 기능 가져오기...로 이동하여 Visual Studio 설치 관리자를 엽니다. .NET 데스크톱 개발 워크로드를 선택한 후 수정을 클릭하세요.

    새 프로젝트 구성 창에서 이름을 입력하거나 프로젝트 이름 상자에 기본 이름을 사용합니다. 그리고 다음을 선택합니다.

    .NET의 경우 권장 대상 프레임워크 또는 .NET 8을 선택한 다음 만들기를 선택합니다.

    새 콘솔 프로젝트가 나타납니다. 프로젝트를 만든 후 원본 파일이 나타납니다.

  2. 프로젝트에서 .cs 코드 파일을 엽니다. 해당 내용을 삭제하여 빈 코드 파일을 만듭니다.

  3. 선택한 언어에 대해 다음 코드를 빈 코드 파일에 붙여넣습니다.

    using System.Diagnostics;
    
    namespace AsyncTasks_SyncOverAsync
    {
         class Jungle
         {
             public static async Task<int> FindBananas()
             {
                 await Task.Delay(1000);
                 Console.WriteLine("Got bananas.");
                 return 0;
             }
    
             static async Task Gorilla_Start()
             {
                 Debugger.Break();
                 Gorilla koko = new Gorilla();
                 int result = await Task.Run(koko.WakeUp);
             }
    
             static async Task Main(string[] args)
             {
                 List<Task> tasks = new List<Task>();
                 for (int i = 0; i < 2; i++)
                 {
                     Task task = Gorilla_Start();
                     tasks.Add(task);
    
                 }
                 await Task.WhenAll(tasks);
    
             }
         }
    
         class Gorilla
         {
    
             public async Task<int> WakeUp()
             {
                 int myResult = await MorningWalk();
    
                 return myResult;
             }
    
             public async Task<int> MorningWalk()
             {
                 int myResult = await Jungle.FindBananas();
                 GobbleUpBananas(myResult);
    
                 return myResult;
             }
    
             /// <summary>
             /// Calls a .Wait.
             /// </summary>
             public void GobbleUpBananas(int food)
             {
                 Console.WriteLine("Trying to gobble up food synchronously...");
    
                 Task mb = DoSomeMonkeyBusiness();
                 mb.Wait();
    
             }
    
             public async Task DoSomeMonkeyBusiness()
             {
                 Debugger.Break();
                 while (!System.Diagnostics.Debugger.IsAttached)
                 {
                     Thread.Sleep(100);
                 }
    
                 await Task.Delay(30000);
                 Console.WriteLine("Monkey business done");
             }
         }
    }
    

    코드 파일을 업데이트한 후 변경 내용을 저장하고 솔루션을 빌드합니다.

  4. 파일 메뉴에서 모두 저장을 선택합니다.

  5. 빌드 메뉴에서 솔루션 빌드를 선택합니다.

병렬 스택 창의 작업 보기 사용

  1. 디버그 메뉴에서 디버깅 시작(또는 F5)을 선택하고 첫 번째 Debugger.Break() 항목이 적중될 때까지 기다립니다.

  2. F5 키를 한 번 누르면 디버거가 같은 Debugger.Break() 줄에서 다시 일시 중지됩니다.

    두 번째 비동기 작업에서 발생하는 Gorilla_Start의 두 번째 호출 시 일시 중지됩니다.

  3. Windows > 병렬 스택 디버그 > 를 선택하여 병렬 스택 창을 연 다음 창의 보기 드롭다운에서 작업을 선택합니다.

    병렬 스택 창의 작업 보기 스크린샷

    비동기 호출 스택에 대한 레이블은 2개의 비동기 논리 스택을 설명합니다. 마지막으로 F5 키를 누르면 다른 작업을 시작했습니다. 복잡한 앱에서 간소화를 위해 동일한 비동기 호출 스택이 단일 시각적 표현으로 그룹화됩니다. 이는 특히 많은 작업이 있는 시나리오에서 보다 완전한 정보를 제공합니다.

    작업 보기와 달리 호출 스택 창에는 여러 작업이 아닌 현재 스레드에 대한 호출 스택만 표시됩니다. 앱 상태를 보다 완벽하게 파악하기 위해 두 항목을 함께 보는 것이 도움이 되는 경우가 많습니다.

    호출 스택의 스크린샷.

    팁 (조언)

    호출 스택 창은 Async cycle를 사용하여 교착 상태와 같은 정보를 설명할 수 있습니다.

    디버깅하는 동안 외부 코드가 표시되는지 여부를 전환할 수 있습니다. 기능을 전환하려면 호출 스택 창의 이름 테이블 머리글을 마우스 오른쪽 단추로 클릭한 다음 외부 코드 표시를 선택하거나 선택 취소합니다. 외부 코드를 표시하는 경우에도 이 연습을 사용할 수 있지만 결과는 그림과 다를 수 있습니다.

  4. F5 키를 다시 누르면 디버거가 메서드에서 DoSomeMonkeyBusiness 일시 중지됩니다.

    F5 이후의 작업 보기 스크린샷

    이 보기는 await 및 유사한 메서드를 사용할 때, 내부 연속 체인에 더 많은 비동기 메서드가 추가되면서 더 완전한 비동기 호출 스택을 보여 줍니다. DoSomeMonkeyBusiness 비동기 메서드이지만 연속 체인에 아직 추가되지 않았으므로 비동기 호출 스택의 맨 위에 있을 수도 있습니다. 다음 단계에서 이러한 경우가 발생하는 이유를 살펴보겠습니다.

    이 보기에는 차단된 Jungle.Main상태 아이콘도 표시됩니다. 이는 정보를 제공하지만 일반적으로 문제를 나타내지는 않습니다. 차단된 작업은 다른 작업이 완료될 때까지 대기 중이거나, 신호를 받을 이벤트 또는 잠금이 해제되기 때문에 차단된 작업입니다.

  5. 메서드를 GobbleUpBananas 마우스로 가리키면 작업을 실행하는 두 스레드에 대한 정보를 가져옵니다.

    호출 스택과 연결된 스레드의 스크린샷.

    현재 스레드는 디버그 도구 모음의 스레드 목록에도 나타납니다.

    디버그 도구 모음의 현재 스레드 스크린샷

    스레드 목록을 사용하여 디버거 컨텍스트를 다른 스레드로 전환할 수 있습니다.

  6. F5 키를 다시 누르면 두 번째 작업에 대한 메서드에서 DoSomeMonkeyBusiness 디버거가 일시 중지됩니다.

    두 번째 F5 이후의 작업 보기 스크린샷

    작업 실행 타이밍에 따라 이 시점에서 별도의 비동기 호출 스택 또는 그룹화된 비동기 호출 스택이 표시됩니다.

    앞의 그림에서 두 작업에 대한 비동기 호출 스택은 동일하지 않으므로 별개입니다.

  7. F5 키를 다시 누르면 긴 지연이 발생하고 작업 보기에 비동기 호출 스택 정보가 표시되지 않습니다.

    지연은 장기 실행 작업으로 인해 발생합니다. 이 예제에서는 웹 요청과 같은 장기 실행 작업을 시뮬레이션하여 스레드 풀이 부족할 수 있습니다. 작업이 차단될 수 있더라도 현재 디버거에서 일시 중지되지 않으므로 작업 보기에 아무것도 표시되지 않습니다.

    팁 (조언)

    모두 중단 단추는 교착 상태가 발생하거나 모든 작업 및 스레드가 현재 차단된 경우 호출 스택 정보를 가져오는 좋은 방법입니다.

  8. 디버그 도구 모음의 IDE 맨 위에서 모두 중단 단추(일시 중지 아이콘), Ctrl + Alt + Break를 선택합니다.

    모두 중단을 선택한 후의 작업 보기 스크린샷

    작업 보기에서 비동기 호출 스택의 위쪽 근처에 GobbleUpBananas이(가) 차단된 것을 볼 수 있습니다. 실제로 두 작업은 같은 지점에서 차단됩니다. 차단된 작업이 반드시 예기치 않은 것은 아니며 반드시 문제가 있는 것은 아닙니다. 그러나 관찰된 실행 지연은 문제를 나타내며 여기에 있는 호출 스택 정보에는 문제의 위치가 표시됩니다.

    이전 스크린샷의 왼쪽에서 말린 녹색 화살표는 현재 디버거 컨텍스트를 나타냅니다. GobbleUpBananas 메서드에서 두 작업이 mb.Wait()에 의해 차단됩니다.

    호출 스택 창에는 현재 스레드가 차단된 것도 표시됩니다.

    모두 중단을 선택한 후 호출 스택의 스크린샷

    동기 호출 GobbleUpBananas 내에서 스레드를 차단하는 호출은 Wait()입니다. 이는 동기화 비동기 안티패턴의 예이며, UI 스레드 또는 대규모 처리 워크로드에서 발생한 경우 일반적으로 다음을 사용하는 await코드 수정으로 해결됩니다. 자세한 내용은 스레드 풀 부족 디버그를 참조하세요. 프로파일링 도구를 사용하여 스레드 풀 부족 문제를 디버그하려면 사례 연구: 성능 문제 격리를 참조하세요.

    또한 흥미로운 점으로, DoSomeMonkeyBusiness는 호출 스택에 나타나지 않습니다. 현재는 실행되지 않고 예약되어 있으므로 작업 보기의 비동기 호출 스택에만 표시됩니다.

    팁 (조언)

    디버거는 스레드별로 코드로 구분됩니다. 예를 들어 F5 키를 눌러 실행을 계속하면 앱이 다음 중단점에 도달하면 다른 스레드의 코드로 분리할 수 있습니다. 디버깅을 위해 이를 관리해야 하는 경우 중단점을 추가하거나, 조건부 중단점을 추가하거나, 모두 중단점을 사용할 수 있습니다. 이 동작에 대한 자세한 내용은 조건부 중단점이 있는 단일 스레드 팔로우를 참조하세요.

샘플 코드 수정

  1. 메서드를 GobbleUpBananas 다음 코드로 바꿉다.

     public async Task GobbleUpBananas(int food) // Previously returned void.
     {
         Console.WriteLine("Trying to gobble up food...");
    
         //Task mb = DoSomeMonkeyBusiness();
         //mb.Wait();
         await DoSomeMonkeyBusiness();
     }
    
  2. MorningWalk 메서드에서 await를 사용하여 GobbleUpBananas를 호출합니다.

    await GobbleUpBananas(myResult);
    
  3. 다시 시작 단추(Ctrl + Shift + F5)를 선택한 다음 앱이 "중단"될 때까지 F5 키를 여러 번 누릅니다.

  4. 모두 중단을 누릅니다.

    이번에는 GobbleUpBananas 비동기적으로 실행됩니다. 중단하면 비동기 호출 스택이 표시됩니다.

    코드 수정 후 디버거 컨텍스트의 스크린샷

    호출 스택 창은 ExternalCode 항목을 제외하고 비어 있습니다.

    코드 편집기에서는 모든 스레드가 외부 코드를 실행하고 있음을 나타내는 메시지를 제공하는 것을 제외하고는 아무것도 표시하지 않습니다.

    그러나 작업 보기는 유용한 정보를 제공합니다. DoSomeMonkeyBusiness 는 예상대로 비동기 호출 스택의 맨 위에 있습니다. 이는 장기 실행 메서드가 있는 위치를 올바르게 알려줍니다. 이는 호출 스택 창의 실제 호출 스택이 충분한 세부 정보를 제공하지 않는 경우 비동기/대기 문제를 격리하는 데 유용합니다.

요약

이 연습에서는 병렬 스택 디버거 창을 시연했습니다. 비동기/대기 패턴을 사용하는 앱에서 이 창을 사용합니다.