다음을 통해 공유


스레드 뷰를 사용하여 교착 상태 디버그

이 자습서에서는 병렬 스택 창의 스레드 뷰를 사용하여 C# 다중 스레드 애플리케이션을 디버그하는 방법을 보여 줍니다. 이 창은 다중 스레드 코드의 런타임 동작을 이해하고 확인하는 데 도움이 됩니다.

스레드 보기는 C++ 및 Visual Basic에도 지원되므로 C#에 대해 이 문서에 설명된 동일한 원칙도 C++ 및 Visual Basic에도 적용됩니다.

스레드 뷰를 사용하면 다음을 수행할 수 있습니다.

  • 여러 스레드에 대한 호출 스택 시각화를 확인합니다. 이 시각화는 현재 스레드에 대한 호출 스택만 표시하는 호출 스택 창보다 앱 상태에 대한 보다 완전한 그림을 제공합니다.

  • 차단되거나 교착 상태인 스레드와 같은 문제를 식별하는 데 도움이 됩니다.

C# 샘플

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

이 샘플에는 두 스레드가 서로 대기할 때 발생하는 교착 상태의 예가 포함되어 있습니다.

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

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

다중 스레드 호출 스택

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

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

호출 스택의 그룹화 그림.

샘플 프로젝트 만들기

프로젝트를 만들려면 다음을 수행합니다.

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

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

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

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

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

    Note

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

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

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

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

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

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

     using System.Diagnostics;
    
     namespace Multithreaded_Deadlock
     {
         class Jungle
         {
             public static readonly object tree = new object();
             public static readonly object banana_bunch = new object();
             public static Barrier barrier = new Barrier(2);
    
             public static int FindBananas()
             {
                 // Lock tree first, then banana
                 lock (tree)
                 {
                     lock (banana_bunch)
                     {
                         Console.WriteLine("Got bananas.");
                         return 0;
                     }
                 }
             }
    
             static void Gorilla_Start(object lockOrderObj)
             {
                 Debugger.Break();
                 bool lockTreeFirst = (bool)lockOrderObj;
                 Gorilla koko = new Gorilla(lockTreeFirst);
                 int result = 0;
                 var done = new ManualResetEventSlim(false);
    
                 Thread t = new Thread(() =>
                 {
                     result = koko.WakeUp();
                     done.Set();
                 });
                 t.Start();
                 done.Wait();
             }
    
             static void Main(string[] args)
             {
                 List<Thread> threads = new List<Thread>();
                 // Start two threads with opposite lock orders
                 threads.Add(new Thread(Gorilla_Start));
                 threads[0].Start(true);  // First gorilla locks tree then banana
                 threads.Add(new Thread(Gorilla_Start));
                 threads[1].Start(false); // Second gorilla locks banana then tree
    
                 foreach (var t in threads)
                 {
                     t.Join();
                 }
             }
         }
    
         class Gorilla
         {
             private readonly bool lockTreeFirst;
    
             public Gorilla(bool lockTreeFirst)
             {
                 this.lockTreeFirst = lockTreeFirst;
             }
    
             public int WakeUp()
             {
                 int myResult = MorningWalk();
                 return myResult;
             }
    
             public int MorningWalk()
             {
                 Debugger.Break();
                 if (lockTreeFirst)
                 {
                     lock (Jungle.tree)
                     {
                         Jungle.barrier.SignalAndWait(5000); // For thread timing consistency in sample
                         Jungle.FindBananas();
                         GobbleUpBananas();
                     }
                 }
                 else
                 {
                     lock (Jungle.banana_bunch)
                     {
                         Jungle.barrier.SignalAndWait(5000); // For thread timing consistency in sample
                         Jungle.FindBananas();
                         GobbleUpBananas();
                     }
                 }
                 return 0;
             }
    
             public void GobbleUpBananas()
             {
                 Console.WriteLine("Trying to gobble up food...");
                 DoSomeMonkeyBusiness();
             }
    
             public void DoSomeMonkeyBusiness()
             {
                 Thread.Sleep(1000);
                 Console.WriteLine("Monkey business done");
             }
         }
     }
    

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

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

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

병렬 스택 창의 스레드 보기 사용

디버깅을 시작하려면:

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

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

    두 번째 스레드 내에서 발생하는 Gorilla_Start의 두 번째 호출에서 일시 중지됩니다.

    Tip

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

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

    병렬 스택 창의 스레드 보기 스크린샷

    스레드 보기에서는 현재 스레드의 스택 프레임 및 호출 경로가 파란색으로 강조 표시됩니다. 스레드의 현재 위치는 노란색 화살표로 표시됩니다.

    호출 스택 Gorilla_Start 의 레이블은 2개 스레드입니다. 마지막으로 F5 키를 누르면 다른 스레드를 시작했습니다. 복잡한 앱에서 간소화를 위해 동일한 호출 스택이 단일 시각적 표현으로 그룹화됩니다. 이렇게 하면 특히 스레드가 많은 시나리오에서 잠재적으로 복잡한 정보가 간소화됩니다.

    디버깅하는 동안 외부 코드가 표시되는지 여부를 전환할 수 있습니다. 기능을 토글하려면 외부 코드 표시를 선택하거나 선택 취소합니다. 외부 코드를 표시하는 경우에도 이 연습을 사용할 수 있지만 결과는 그림과 다를 수 있습니다.

  4. F5 키를 다시 누르면 디버거가 Debugger.Break() 줄의 MorningWalk 메서드에서 일시 중지됩니다.

    병렬 스택 창에는 메서드에서 현재 실행 중인 스레드의 위치가 MorningWalk 표시됩니다.

    F5 이후의 스레드 보기 스크린샷

  5. 그룹화된 호출 스택에 의해 나타내는 두 스레드에 대한 정보를 얻으려면 MorningWalk 메서드 위에 마우스를 올려놓습니다.

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

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

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

    스레드 목록을 사용하여 디버거 컨텍스트를 다른 스레드로 전환할 수 있습니다. 이렇게 하면 현재 실행 중인 스레드가 변경되지 않고 디버거 컨텍스트만 변경됩니다.

    또한 스레드 보기에서 메서드를 두 번 클릭하거나 스레드 보기에서 메서드를 마우스 오른쪽 버튼으로 클릭한 후 프레임으로 전환>[스레드 ID]를 선택하여 디버거 컨텍스트를 전환할 수 있습니다.

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

    두 번째 F5 이후의 스레드 보기 스크린샷

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

    앞의 그림에서 두 스레드에 대한 호출 스택은 부분적으로 그룹화됩니다. 호출 스택의 동일한 세그먼트가 그룹화되고 화살표 선은 구분된 세그먼트(즉, 동일하지 않음)를 가리킵니다. 현재 스택 프레임은 파란색 강조 표시로 표시됩니다.

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

    지연은 교착 상태 때문에 발생합니다. 스레드가 차단될 수 있더라도 현재 디버거에서 일시 중지되지 않으므로 스레드 보기에는 아무 것도 나타나지 않습니다.

    Tip

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

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

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

    스레드 보기 FindBananas 의 호출 스택 맨 위에 교착 상태가 표시됩니다. 실행 포인터 FindBananas 는 현재 디버거 컨텍스트를 나타내는 curled 녹색 화살표이지만 스레드가 현재 실행되고 있지 않다는 것을 알려줍니다.

    코드 편집기에서 lock 함수에서 말린 녹색 화살표를 찾습니다. 두 스레드는 메서드 lock의 함수 FindBananas에서 차단됩니다.

    모두 중단을 선택한 후 코드 편집기의 스크린샷

    스레드 실행 순서에 따라 교착 상태가 lock(tree) 문 또는 lock(banana_bunch) 문에 나타납니다.

    lock 호출은 FindBananas 메서드에서 스레드를 차단합니다. 한 스레드는 다른 스레드가 tree의 잠금을 해제하기를 기다리고 있지만, 다른 스레드는 banana_bunch의 잠금을 해제하기 전에 tree의 잠금이 해제되기를 기다리고 있습니다. 이는 두 스레드가 서로 대기할 때 발생하는 클래식 교착 상태의 예입니다.

    Copilot를 사용하는 경우 잠재적인 교착 상태를 식별하는 데 도움이 되는 AI 생성 스레드 요약을 가져올 수도 있습니다.

    코필로트 스레드 요약 설명의 스크린샷

샘플 코드 수정

이 코드를 수정하려면 항상 모든 스레드에서 일관되고 전역적인 순서로 여러 잠금을 획득합니다. 이렇게 하면 순환 대기가 방지되고 교착 상태가 제거됩니다.

  1. 교착 상태를 해결하려면 코드를 MorningWalk 다음 코드로 바꿉다.

    public int MorningWalk()
    {
        Debugger.Break();
        // Always lock tree first, then banana_bunch
        lock (Jungle.tree)
        {
            Jungle.barrier.SignalAndWait(5000); // OK to remove
            lock (Jungle.banana_bunch)
            {
                Jungle.FindBananas();
                GobbleUpBananas();
            }
        }
        return 0;
    }
    
  2. 앱을 다시 시작합니다.

Summary

이 연습에서는 병렬 스택 디버거 창을 시연했습니다. 다중 스레드 코드를 사용하는 실제 프로젝트에서 이 창을 사용합니다. C++, C#또는 Visual Basic으로 작성된 병렬 코드를 검사할 수 있습니다.