Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

(시리즈 글이 2개 있습니다.)
.NET Framework: 561. null 처리된 객체가 왜 GC에 의해 수집되지 않을까요?
; https://www.sysnet.pe.kr/2/0/10920

닷넷: 2154. C# - 네이티브 자원을 포함한 관리 개체(예: 스레드)의 GC 정리
; https://www.sysnet.pe.kr/2/0/13435




C# - 네이티브 자원을 포함한 관리 개체(예: 스레드)의 GC 정리

아래와 같은 질문이 있었군요. ^^

작업이 종료된 Thread의 객체는 가비지 컬렉터가 삭제 시키나요??
; https://forum.dotnetdev.kr/t/thread/8289

위의 상황을 설명하는 코드를 우선, native 자원이 없는 개체를 만들어 유사하게 만들어 보면 아래와 같습니다.

namespace ConsoleApp1;

internal class Program
{
    static void Main(string[] args)
    {
        Program instance;

        while (true)
        {
            Program p = new Program();
            instance = p;

            Thread.Sleep(1000);
        }
    }
}

위와 같은 경우에, Program 개체는 끊임없이 생성되면서 지역 변수인 instance에 대입이 되고 있습니다. 그렇다면, 마지막 개체를 제외하고 그 이전의 개체들이 GC가 구동되면 살아 있게 될까요?

사실, 원칙만 알고 있다면 어렵게 생각할 것이 없는 문제입니다. 여기서 그 원칙이란 바로 "해당 개체를 참조하고 있는 root 개체"가 있느냐는 것입니다.

루프를 한 번 돌았을 때, "Object#1"은 instance 지역 변수가 root 개체로 참조를 하고 있기 때문에 당연히 GC가 될 수 없습니다. 반면, 그 이후에 "Object#2"가 생성돼 instance 지역 변수에 할당되면 이전의 "Object#1"은 지역 변수에서 더 이상 가리키고 있지 않기 때문에 root 참조 개체가 없는 상태로 바뀝니다.

결국, 가장 마지막의 개체만 빼고는 이전에 생성한 모든 개체들이 GC 대상이 돼 해제가 됩니다. 어렵지 않죠? ^^




자, 그런데 Thread는 왠지 뭔가 복잡해 보이는 타입으로 느껴집니다. 왜냐하면 운영체제의 스레드 자원과 엮여 있기 때문입니다. 하지만, 여기서도 원칙은 같습니다. GC는 그저 "해당 개체를 참조하고 있는 root 개체"가 없으면 무조건 해제한다는 것입니다.

그런 관점에서 Thread를 간단하게 재현해 보면 Native 자원을 하나 품고 있는 타입으로 생각해 볼 수 있습니다. 유사 코드로는 이렇게 작성할 수 있는데요,

using System.Runtime.InteropServices;

namespace ConsoleApp3;

internal class Program
{
    static void Main(string[] args)
    {
        ThreadLike instance;

        while (true)
        {
            ThreadLike p = new ThreadLike();
            instance = p;

            Thread.Sleep(1000);
        }
    }
}

class ThreadLike
{
    IntPtr _ptr;

    public ThreadLike()
    {
        _ptr = Marshal.AllocCoTaskMem(1024); // 1KB 비관리 메모리 할당
    }
}

자, 그럼 위와 같은 상황에서 GC는 ThreadLike 인스턴스를 (마지막 개체를 제외하고) 정리하지 못할까요?

당연히, 정리를 합니다. 하지만, 해제 단계에서 Native 자원까지는 정리하지 않는다는 점이 중요합니다. Thread 역시 마찬가지입니다. 닷넷 세계에서 생성한 Thread 인스턴스는 엄밀히 Native 세계의 스레드 종료 여부와 무관하게 정리될 수 있습니다.

진짜로 그런지 아래와 같은 코드로 테스트를 해보면,

namespace ConsoleApp1;

internal class Program
{
    static void Main(string[] args)
    {
        CreateThread();
        Console.ReadLine();
    }

    static WeakReference _wr; // GC 여부의 확인을 돕기 위해 약한 참조 변수를 하나 두고,

    private static void CreateThread()
    {
        Thread t = new Thread(new ThreadStart(DoWork));
        t.Start();
        _wr = new WeakReference(t); // 생성한 스레드의 약한 참조를 유지
    }

    private static void DoWork()
    {
        while (true)
        {
            Console.WriteLine(_wr.Target); // 1초 단위로 GC를 수행하면서 스레드 개체가 GC 되었는지 확인
            GC.Collect();       
            Thread.Sleep(1000);
        }
    }
}

화면에는 WeakReference로 걸어두고 있는 Target이 끝까지 살아 있는 것을 볼 수 있습니다.

아니, "스레드 종료 여부"와 상관없이 GC가 수행될 수 있다면서요? 맞습니다. 원래는 t 인스턴스는 (스레드가 실행 중이더라도) GC가 되어야 합니다. 그런데 왜? GC가 안 되는 것일까요?

제가 뭐라고 그랬죠? ^^ "해당 개체를 참조하고 있는 root 개체"가 있으면 GC 정리가 안 된다고 했습니다. 즉, 위의 Thread t 인스턴스는 어딘가에서 참조를 하고 있는 것입니다.




이쯤에서 분석을 위해 windbg의 힘을 빌려보겠습니다. ^^

위의 응용 프로그램에 대해 windbg로 Thread 인스턴스를 heap에서 뒤져보면,

0:007> !name2ee System.Private.CoreLib.dll!System.Threading.Thread
Module:      00007ffca9f74000
Assembly:    System.Private.CoreLib.dll
Token:       00000000020002D3
MethodTable: 00007ffcaa1b4330
EEClass:     00007ffcaa10af80
Name:        System.Threading.Thread

0:007> !dumpheap -mt 00007ffcaa1b4330
         Address               MT           Size
    01b727809f08     7ffcaa1b4330             72 
    01b72780a040     7ffcaa1b4330             72 

2개의 인스턴스가 나오는데, 그중 하나에 대해 root 개체를 살펴보면 이렇게 나옵니다.

0:007> !gcroot 01b727809f08
HandleTable:
    000001b724d513a8 (strong handle)
          -> 01b727809f08     System.Threading.Thread 

Found 1 unique roots.

보는 바와 같이, "strong handle" 유형의 자원에서 Thread 인스턴스에 대한 참조를 유지하고 있습니다. 실제로 GC Handle 영역을 뒤져보면,

0:007> !gchandles
          Handle Type                  Object     Size             Data Type
...[생략]...
000001B724D513A0 Strong      000001b727809fb0      144                  System.Object[]
000001B724D513A8 Strong      000001b727809f08       72                  System.Threading.Thread
000001B724D513B0 Strong      000001b725002430     8184                  System.Object[]
000001B724D513C0 Strong      000001b725002018     1048                  System.Object[]
000001B724D513C8 Strong      000001b727800180      128                  System.ExecutionEngineException
000001B724D513D0 Strong      000001b727800100      128                  System.StackOverflowException
000001B724D513D8 Strong      000001b727800080      128                  System.OutOfMemoryException
000001B724D513E0 Strong      000001b727800020       96                  System.Int32[]
000001B724D513E8 Strong      000001b725000020     8184                  System.Object[]
000001B724D513F8 Strong      000001b72780a040       72                  System.Threading.Thread
000001B724D515F8 Pinned      000001b727800200       24                  System.Object

Statistics:
              MT    Count    TotalSize Class Name
00007ffca9f793b8        1           24 System.Object
00007ffcaa11bf38        1           96 System.Int32[]
00007ffcaa1b32b8        1          128 System.ExecutionEngineException
00007ffcaa1b31b8        1          128 System.StackOverflowException
00007ffcaa1b30b8        1          128 System.OutOfMemoryException
00007ffcaa229410        1          184 System.Diagnostics.Tracing.NativeRuntimeEventSource
00007ffcaa223550        4          256 Interop+Advapi32+EtwEnableCallback
00007ffcaa1b4330        5          360 System.Threading.Thread
00007ffcaa221500        1          384 System.Diagnostics.Tracing.RuntimeEventSource
00007ffcaa09aa68        4        17560 System.Object[]
Total 20 objects

Handles:
    Strong Handles:       10
    Pinned Handles:       1
    Weak Long Handles:    4
    Weak Short Handles:   5

Strong 타입의 Handle 값 하나가 스레드 자원에 대한 GC Heap 주솟값을 가리키고 있는 것을 볼 수 있습니다. 바로 저 루트 개체가 GC를 막고 있는 건데요, (확인해 본 것은 아니지만 아마도) 스레드 종료 시까지 저 값을 유지시키는 듯하기 때문에 결국 네이티브 스레드가 종료하지 않는 한 그와 대응하는 관리 개체인 Thread 인스턴스도 GC가 안 되는 것입니다.




참고로, 지난 글에서 "strong handle"로 등록되는 자원 중의 한 유형이 "ThreadStatic" 인스턴스라고 설명한 적이 있습니다.

Windbg - ThreadStatic 필드 값을 조사하는 방법
; https://www.sysnet.pe.kr/2/0/13414

재미있게도 Thread 타입의 소스 코드를 보면,

// 닷넷 버전에 따라 다를 수 있습니다.
// https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs,3980e012bae82e96
namespace System.Threading
{
    public sealed partial class Thread : CriticalFinalizerObject
    {
        private static AsyncLocal? s_asyncLocalPrincipal;

        [ThreadStatic]
        private static Thread? t_currentThread;
        
        // ...[생략]...
    }
}

저렇게 ThreadStatic 유형으로 Thread? t_currentThread 변수가 있기는 한데요, 이것과 실제 Thread 타입을 가리키는 GC Handle과는 아무런 관련이 없습니다. t_currentThread 필드는 Thread.CurrentThread에 대한 cache 용도로만 사용되는데,

private static Thread InitializeCurrentThread() => t_currentThread = GetCurrentThreadNative();
 
public static Thread CurrentThread
{
    [Intrinsic]
    get
    {
        return t_currentThread ?? InitializeCurrentThread();
    }
}

확인을 위해 다음과 같은 소스 코드로 확인해 보면,

Thread t = new Thread((arg) =>
{
    var threadField = typeof(Thread).GetField("t_currentThread", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
    Console.WriteLine($"{threadField.GetValue(null) ?? "(null)"}"); // "(null)" 출력

    Thread.CurrentThread.Name = $"Thread {number}";
    
    Console.WriteLine($"{threadField.GetValue(null) ?? "(null)"}"); // "System.Threading.Thread" 출력

    Console.ReadLine();
});

결과에서 보듯이, CurrentThread 필드를 접근해야만 값이 채워지는 것을 볼 수 있습니다.




암튼, 닷넷 런타임에서는 Thread 자원에 대해서는 (다른 여타 native 자원과는 달리) 특별한 루트 유지를 하고 있다는 점! (심지어 커널에서도 스레드 자원의 해제는 특별합니다.)




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







[최초 등록일: ]
[최종 수정일: 11/6/2023]

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

비밀번호

댓글 작성자
 




1  2  3  4  5  6  [7]  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13457정성태11/25/20232277VS.NET IDE: 187. Visual Studio - 16.9 버전부터 추가된 "Display inline type hints" 옵션
13456정성태11/25/20232580닷넷: 2169. C# - OpenAI를 사용해 PDF 데이터를 대상으로 OpenAI 챗봇 작성 [1]파일 다운로드1
13455정성태11/25/20232472닷넷: 2168. C# - Azure.AI.OpenAI 패키지로 OpenAI 사용파일 다운로드1
13454정성태11/23/20232823닷넷: 2167. C# - Qdrant Vector DB를 이용한 Embedding 벡터 값 보관/조회 (Azure OpenAI) [1]파일 다운로드1
13453정성태11/23/20232320오류 유형: 879. docker desktop 설치 시 "Invalid JSON string. (Exception from HRESULT: 0x83750007)"
13452정성태11/22/20232424닷넷: 2166. C# - Azure OpenAI API를 이용해 사용자가 제공하는 정보를 대상으로 검색하는 방법파일 다운로드1
13451정성태11/21/20232554닷넷: 2165. C# - Azure OpenAI API를 이용해 ChatGPT처럼 동작하는 콘솔 응용 프로그램 제작파일 다운로드1
13450정성태11/21/20232364닷넷: 2164. C# - Octokit을 이용한 GitHub Issue 검색파일 다운로드1
13449정성태11/21/20232441개발 환경 구성: 688. Azure OpenAI 서비스 신청 방법
13448정성태11/20/20232678닷넷: 2163. .NET 8 - Dynamic PGO를 결합한 성능 향상파일 다운로드1
13447정성태11/16/20232562닷넷: 2162. ASP.NET Core 웹 사이트의 SSL 설정을 코드로 하는 방법
13446정성태11/16/20232513닷넷: 2161. .NET Conf 2023 - Day 1 Blazor 개요 정리
13445정성태11/15/20232831Linux: 62. 리눅스/WSL에서 CA 인증서를 저장하는 방법
13444정성태11/15/20232589닷넷: 2160. C# 12 - Experimental 특성 지원
13443정성태11/14/20232613개발 환경 구성: 687. OpenSSL로 생성한 사용자 인증서를 ASP.NET Core 웹 사이트에 적용하는 방법
13442정성태11/13/20232432개발 환경 구성: 686. 비주얼 스튜디오로 실행한 ASP.NET Core 사이트를 WSL 2 인스턴스에서 https로 접속하는 방법
13441정성태11/12/20232748닷넷: 2159. C# - ASP.NET Core 프로젝트에서 서버 Socket을 직접 생성하는 방법파일 다운로드1
13440정성태11/11/20232420Windows: 253. 소켓 Listen 시 방화벽의 Public/Private 제어 기능이 비활성화된 경우
13439정성태11/10/20232946닷넷: 2158. C# - 소켓 포트를 미리 시스템에 등록/예약해 사용하는 방법(Port Exclusion Ranges)파일 다운로드1
13438정성태11/9/20232539닷넷: 2157. C# - WinRT 기능을 이용해 윈도우에서 실행 중인 Media App 제어
13437정성태11/8/20232733닷넷: 2156. .NET 7 이상의 콘솔 프로그램을 (dockerfile 없이) 로컬 docker에 배포하는 방법
13436정성태11/7/20232974닷넷: 2155. C# - .NET 8 런타임부터 (Reflection 없이) 특성을 이용해 public이 아닌 멤버 호출 가능
13435정성태11/6/20232903닷넷: 2154. C# - 네이티브 자원을 포함한 관리 개체(예: 스레드)의 GC 정리
13434정성태11/1/20232668스크립트: 62. 파이썬 - class의 정적 함수를 동적으로 교체
13433정성태11/1/20232380스크립트: 61. 파이썬 - 함수 오버로딩 미지원
13432정성태10/31/20232455오류 유형: 878. 탐색기의 WSL 디렉터리 접근 시 "Attempt to access invalid address." 오류 발생
1  2  3  4  5  6  [7]  8  9  10  11  12  13  14  15  ...