Microsoft MVP성태의 닷넷 이야기
글쓴 사람
홈페이지
첨부 파일

.NET 응용 프로그램에 기본 생성되는 스레드들에 대한 탐구

이론을 배웠으면, 검증하고 싶은 마음이 생기는데요. ^^ 이번엔, .NET 응용 프로그램에서 생성되는 '기본 스레드들'에 대해서 탐구해 볼까 합니다.

복잡한 응용 프로그램에서는 테스트 하기가 어려우니까, 간단하게 "Console Application"으로 분석을 해보겠습니다.

검증 작업을 돕기 위해 몇 가지 유틸리티 함수가 필요한데요. 우선, 현재 프로그램에 실행 중인 스레드 목록을 다음과 같이 확인해 볼 수 있습니다.

static void OutputThreads()
{
    Console.WriteLine();
    int n = 0;
    foreach (ProcessThread thread in Process.GetCurrentProcess().Threads)
    {
        Console.WriteLine("[" + n + "] " + thread.Id + ", (0x" + thread.Id.ToString("x") + ")");
        n++;
    }
}

실행해 보면, 다음과 같은 식으로 결과를 보여줍니다.

[0] 6712, (0x1a38)
[1] 8976, (0x2310)
[2] 9244, (0x241c)
[3] 2280, (0x8e8)

이렇게 실행된 프로세스에 windbg 로 연결하면 스레드 목록을 확인할 수 있습니다.

0:004> ~*
   0  Id: 52c.1a38 Suspend: 1 Teb: 7efdd000 Unfrozen
      Start: *** WARNING: Unable to verify checksum for D:\...\Debug\ConsoleApplication1.exe
ConsoleApplication1!COM+_Entry_Point <PERF> (ConsoleApplication1+0x2a9e) (011c2a9e) 
      Priority: 0  Priority class: 32  Affinity: ff
   1  Id: 52c.2310 Suspend: 1 Teb: 7efda000 Unfrozen
      Start: clr!DebuggerRCThread::ThreadProcStatic (7261b30c) 
      Priority: 0  Priority class: 32  Affinity: ff
   2  Id: 52c.241c Suspend: 1 Teb: 7efd7000 Unfrozen
      Start: clr!Thread::intermediateThreadProc (726fc018) 
      Priority: 2  Priority class: 32  Affinity: ff
   3  Id: 52c.08e8 Suspend: 1 Teb: 7efaf000 Unfrozen
      Start: ntdll!TppWaiterpThread (77a541f3) 
      Priority: 0  Priority class: 32  Affinity: ff
.  4  Id: 52c.128c Suspend: 1 Teb: 7efac000 Unfrozen
      Start: ntdll!DbgUiRemoteBreakin (77aaf7ea) 
      Priority: 0  Priority class: 32  Affinity: ff

원래 4개였다가 windbg 를 붙이면서 5개가 되었는데, 마지막 4번 Start 함수의 ntdll!DbgUiRemoteBreakin 이라는 이름에서 디버거 용 스레드가 해당 프로세스에 생성된 것임을 미뤄 짐작할 수 있습니다. (따라서 4번은 무시하고!)

그 외에, 0번은 Console Application 의 '주 스레드'임이 확실할 것 같고.

남은 것은 1~3번 스레드이지만, SOS 확장 DLL의 도움을 받아서 "!threads" 명령어를 실행해 보면 다음과 같이 2번 스레드가 "Finalizer" 라는 것도 알 수 있습니다.

0:004> !threads
ThreadCount:      2
UnstartedThread:  0
BackgroundThread: 1
PendingThread:    0
DeadThread:       0
Hosted Runtime:   no
                                   PreEmptive   GC Alloc                Lock
       ID  OSID ThreadOBJ    State GC           Context       Domain   Count APT Exception
   0    1  1a38 006c7710      a020 Enabled  02822310:02823fe8 006bc7f0     1 MTA
   2    2  241c 006d25e0      b220 Enabled  00000000:00000000 006bc7f0     0 MTA (Finalizer)

자... 이제 남은 것은 1번과 3번 스레드군요.




사실, 제가 목표한 것은 1번과 3번 중의 하나는 Concurrent GC 스레드일 것이라고 생각했었습니다. 기본적으로 app.config 에 아무런 GC 설정없이 응용 프로그램을 실행하면 적용되는 것이 Concurrent-GC 인데, 별도로 2세대 힙만을 전용으로 GC 하는 스레드가 존재하는 유형입니다. 이에 대해서는 유경상 님이 쓰신 GC 관련 글에서 다음의 이미지가 잘 나타내 주고 있습니다.

[Concurrent-GC 모드 (출처: http://www.simpleisbest.net/post/2011/04/25/Garbage-Collection-Modes.aspx)]
thread_type_1.png

그리고, 또 한가지는 (제 예상으로) ThreadPool 을 담당하는 스레드입니다. 전용 ThreadPool 을 만들어 보신 분들은 아시겠지만, ThreadPool 을 구현하려면 그것만을 담당하는 스레드 하나가 있어야만 합니다. 그래야, 스레드 풀 내에 maxThreads 수 만큼 늘어난 스레드 수가 할 일이 없을 때 다시 minThreads 수로 줄어드는 식의 구현을 할 수 있기 때문입니다.

위의 가정을 바탕으로 테스트를 진행해 볼 텐데요. ^^

우선, 그 중에 하나가 ThreadPool 을 관리하는지 테스트 해보려면 어떻게 해야 할까요?

제 생각에는, 보통의 상태에서 ThreadPool.QueueUserWorkItem 메서드를 수행하는 것과 1번과 3번 스레드를 강제로 죽인 상황에서 다시 ThreadPool.QueueUserWorkItem 을 실행한 경우와 비교해 보면 될 것 같았습니다.

그런데, 직접 해보면 2가지 상황 모두 ThreadPool.QueueUserWorkItem 으로 정상적으로 새롭게 스레드가 생성이 되고, 심지어 생성된 스레드들은 할 일을 마치고 정확히 20 초 후에 종료되는 관리적인 면까지 동일하게 수행되었습니다. 아래는 제가 이에 대해 테스트한 간단한 콘솔 응용 프로그램입니다.

[정상적인 스레드들이 운용되고 있을 때의 스레드 풀 사용 변화]
thread_type_2.png

[2개의 스레드를 종료한 이후 스레드 풀을 사용했을 때의 변화]
thread_type_3.png

음... 아쉽게도 증명에 실패했습니다. ^^; 일단 더 테스트 해 볼 수 있는 시나리오가 생각나지 않으므로 이에 대해서는 넘어가고.




그 다음, 1번과 3번 중의 하나는 Concurrent GC 용 스레드 일 것이라는 가정을 테스트 해보겠습니다.

방법은 간단한데요. Concurrent GC 스레드가 2세대 힙을 GC 하는 역할을 하기 때문에 1번과 3번 스레드를 모두 죽인 후, 2 세대 GC 카운트가 여전히 발생하는 지를 테스트 해보면 됩니다. 코드로는 다음의 2가지 메서드를 상황에 따라 실행하면 되는 작업입니다.

private static void HeapAllocation()
{
    Console.WriteLine("Old Count: " + OutputGCCount());
    for (int i = 0; i < 1000; i++)
    {
        byte[] contents = new byte[1024 * 50];
    }
    Console.WriteLine("New Count: " + OutputGCCount());
}

static string OutputGCCount()
{
    int gen0 = GC.CollectionCount(0);
    int gen1 = GC.CollectionCount(1);
    int gen2 = GC.CollectionCount(2);

    return ("Gen 0: " + gen0 + ", Gen1: " + gen1 + ", Gen2: " + gen2);
}

아래는 실제로 제가 테스트 해본 과정입니다.

thread_type_4.png

만약, 그 두 개중의 하나가 Concurrent-GC 였다면 위와 같이 2세대 힙에 대한 GC 가 발생하는 일은 없었어야 합니다. (물론, 스레드의 수에 변화가 없이 2세대 힙에 대한 GC가 발생했습니다.) 음... 이번에도 역시 증명에 실패했군요. ^^;

혹시, Concurrent-GC 를 사용하지 않겠다고 app.config 에 명시하면 어떻게 될까요? 초기 스레드 수가 줄어들까요? 확인을 위해 app.config 에 다음과 같이 명시해봤습니다.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <runtime>
        <gcConcurrent enabled="false" />
    </runtime>
</configuration>

위와 같이 설정하게 되면, Non-Concurrent-GC 가 동작하게 되고 이렇게 되면 응용 프로그램 내의 스레드가 new 를 할 때 마다 힙이 부족한 경우, 그런 상황을 유발한 그 스레드를 제외하고 다른 모든 Managed 스레드들은 멈추게 되고 new 할당을 시도하려던 스레드가 직접 GC 를 수행하기 때문에 별도의 스레드가 필요가 없어서 예상대로라면 3개의 기본 스레드로 응용 프로그램이 시작되어야 합니다.

그러나 실행해 봤지만, 여전히 응용 프로그램 시작 시 4개의 스레드가 생성되는 것에는 변함이 없었습니다.




이 글을 시작할 때만 해도, 4개의 스레드에 대한 모든 정체를 밝히리라 ^^ 다짐했었는데, 결국 2개의 스레드만 정체를 밝히고 나머지는 더욱 혼란만 가중시켜버린 것 같습니다. 혹시, 이 글을 읽는 분들 중에서 나머지 2개에 대한 스레드의 정체를 알고 계신 분이 있거나, 혹은 정체를 밝힐 수 있는 기발한 아이디어가 있다면 덧글 부탁드리겠습니다.

(위에서 제가 한 테스트를 여러분들도 하실 수 있도록, 첨부 파일에 코드를 포함시켜 두었습니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]





[최초 등록일: ]
[최종 수정일: 4/25/2019 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer@outlook.com

비밀번호

댓글 쓴 사람
 



2019-04-24 09시03분
using System;
using System.Diagnostics;
using System.Threading;

namespace Program
{
    public class Program
    {
        public static void Main(string[] args)
        {
            ThreadPool.GetMinThreads(out int workerThreads, out int completionPortThreads);
            Console.WriteLine($"workerThreads: {workerThreads}");

            OutputThreads();

            Console.WriteLine("Attach WinDBG and check!");
            Console.ReadLine();
        }

        static void OutputThreads()
        {
            int id = AppDomain.GetCurrentThreadId();

            Console.WriteLine();
            int n = 0;
            foreach (ProcessThread thread in Process.GetCurrentProcess().Threads)
            {
                string threadType = "(ThreadPool)";
                if (id == thread.Id)
                {
                    threadType = "(Managed)";
                }

                if (thread.BasePriority == 10)
                {
                    threadType = "(Finalizer)";
                }

                Console.WriteLine($"[{n}] {thread.Id}, (0x" + thread.Id.ToString("x") + $") {threadType}");
                n++;
            }
        }
    }
}
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12255정성태7/6/202025.NET Framework: 923. C# - ETW(Event Tracing for Windows)를 이용한 Finalizer 실행 감시파일 다운로드1
12254정성태7/2/202029오류 유형: 626. git - REMOTE HOST IDENTIFICATION HAS CHANGED!
12253정성태7/2/202068.NET Framework: 922. C# - .NET ThreadPool의 Local/Global Queue파일 다운로드1
12252정성태7/2/202061.NET Framework: 921. C# - I/O 스레드를 사용한 비동기 소켓 서버/클라이언트파일 다운로드2
12251정성태7/1/202090.NET Framework: 920. C# - 파일의 비동기 처리 유무에 따른 스레드 상황파일 다운로드2
12250정성태7/1/2020286.NET Framework: 919. C# - 닷넷에서의 진정한 비동기 호출을 가능케 하는 I/O 스레드 사용법 [1]파일 다운로드1
12249정성태6/29/202029오류 유형: 625. Microsoft SQL Server 2019 RC1 Setup - 설치 제거 시 Warning 26003 오류 발생
12248정성태6/29/202025오류 유형: 624. SQL 서버 오류 - service-specific error code 17051
12247정성태6/29/2020111.NET Framework: 918. C# - 불린 형 상수를 반환값으로 포함하는 3항 연산자 사용 시 단축 표현 권장(IDE0075) [2]파일 다운로드1
12246정성태6/29/202056.NET Framework: 917. C# - USB 관련 ETW(Event Tracing for Windows)를 이용한 키보드 입력을 감지하는 방법
12245정성태6/25/2020198.NET Framework: 916. C# - Task.Yield 사용법 (2) [2]파일 다운로드1
12244정성태6/29/202077.NET Framework: 915. ETW(Event Tracing for Windows)를 이용한 닷넷 프로그램의 내부 이벤트 활용파일 다운로드1
12243정성태6/23/202057VS.NET IDE: 147. Visual C++ 프로젝트 - .NET Core EXE를 "Debugger Type"으로 지원하는 기능 추가
12242정성태6/24/202033오류 유형: 623. AADSTS90072 - User account '...' from identity provider 'live.com' does not exist in tenant 'Microsoft Services'
12241정성태6/26/2020105.NET Framework: 914. C# - Task.Yield 사용법파일 다운로드1
12240정성태6/23/202076오류 유형: 622. 소켓 바인딩 시 "System.Net.Sockets.SocketException: An attempt was made to access a socket in a way forbidden by its access permissions" 오류 발생
12239정성태6/21/202057Linux: 30. (윈도우라면 DLL에 속하는) .so 파일이 텍스트로 구성된 사례
12238정성태6/21/202088.NET Framework: 913. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 라이브러리
12237정성태6/20/2020103.NET Framework: 912. 리눅스 환경의 .NET Core에서 "test".IndexOf("\0")가 0을 반환
12236정성태6/19/202063오류 유형: 621. .NET Standard 대상으로 빌드 시 dynamic 예약어에서 컴파일 오류 - error CS0656: Missing compiler required member 'Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create'
12235정성태6/19/202043오류 유형: 620. Windows 10 - Inaccessible boot device 블루 스크린
12234정성태6/19/202043개발 환경 구성: 494. NuGet - nuspec의 패키지 스키마 버전(네임스페이스) 업데이트 방법
12233정성태6/19/202036오류 유형: 619. SQL 서버 - The transaction log for database '...' is full due to 'LOG_BACKUP'. - 두 번째 이야기
12232정성태6/19/202030오류 유형: 618. SharePoint - StoreBusyRetryLater 오류
12231정성태6/15/2020113.NET Framework: 911. Console/Service Application을 위한 SynchronizationContext - AsyncContext
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...