Microsoft MVP성태의 닷넷 이야기
닷넷: 2312. C#, C++ - Windows / Linux 환경의 Thread Name 설정 [링크 복사], [링크+제목 복사],
조회: 5377
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일

(시리즈 글이 5개 있습니다.)
.NET Framework: 308. .NET System.Threading.Thread 개체에서 Native Thread Id를 구할 수 있을까?
; https://www.sysnet.pe.kr/2/0/1244

.NET Framework: 452. .NET System.Threading.Thread 개체에서 Native Thread Id를 구하는 방법 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/1724

.NET Framework: 1189. C# - 런타임 환경에 따라 달라진 AppDomain.GetCurrentThreadId 메서드
; https://www.sysnet.pe.kr/2/0/13024

닷넷: 2311. C# - Windows / Linux 환경에서 Native Thread ID 가져오기
; https://www.sysnet.pe.kr/2/0/13814

닷넷: 2312. C#, C++ - Windows / Linux 환경의 Thread Name 설정
; https://www.sysnet.pe.kr/2/0/13816




C#, C++ - Windows / Linux 환경의 Thread Name 설정

일단 C#의 Thread 타입에는 Name 속성이 있어 아주 쉽게 해결이 가능합니다.

반면, C/C++의 경우에는 어떨까요? 우선 윈도우 환경이라면 SetThreadDescription Win32 API를 사용하면 됩니다. 귀찮으니, ^^; C++ 코드가 아닌 C#으로 다음과 같이 예제를 만들 수 있습니다.

using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;

[Flags]
public enum ThreadAccess : int
{
    TERMINATE = (0x0001),
    SUSPEND_RESUME = (0x0002),
    GET_CONTEXT = (0x0008),
    SET_CONTEXT = (0x0010),
    SET_INFORMATION = (0x0020),
    QUERY_INFORMATION = (0x0040),
    SET_THREAD_TOKEN = (0x0080),
    IMPERSONATE = (0x0100),
    DIRECT_IMPERSONATION = (0x0200),
    THREAD_SET_LIMITED_INFORMATION = (0x0400),
    THREAD_QUERY_LIMITED_INFORMATION = (0x0800)
}

internal class Program
{
    [DllImport("kernel32.dll")]
    static extern IntPtr OpenThread(ThreadAccess dwDesiredAccess, bool bInheritHandle, uint dwThreadId);

    [DllImport("kernel32.dll")]
    static extern bool CloseHandle(IntPtr hObject);

    [DllImport("kernel32.dll")]
    public static extern uint GetCurrentThreadId();

    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern uint SetThreadDescription(IntPtr thread, string description);

    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern uint GetThreadDescription(IntPtr thread, out IntPtr pDescription);

    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern IntPtr LocalFree(IntPtr thread);

    public static uint GetThreadNativeId()
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            return GetCurrentThreadId();
        }

        return 0;
    }

    public static IntPtr GetCurrentThreadHandle()
    {
        return OpenThread(ThreadAccess.THREAD_SET_LIMITED_INFORMATION | ThreadAccess.THREAD_QUERY_LIMITED_INFORMATION, false, GetThreadNativeId());
    }

    public static void SetThreadName(string name)
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            IntPtr handle = GetCurrentThreadHandle();
            SetThreadDescription(handle, name); // THREAD_SET_LIMITED_INFORMATION

#if DEBUG
            GetThreadDescription(handle, out IntPtr pDescription); // THREAD_QUERY_LIMITED_INFORMATION
            string? text = Marshal.PtrToStringAnsi(pDescription); // GetThreadDescription의 결과물은 ANSI 문자열임에 주의!
            Debug.Assert(name == text);

            if (pDescription != IntPtr.Zero)
            {
                LocalFree(pDescription);
            }
#endif

            CloseHandle(handle);

        }
    }

    static void Main(string[] args)
    {
        SetThreadName("Main Thread2");
    }
}

문제는, 저 함수가 Windows 10부터 지원하기 때문에 혹시라도 그 미만의 버전에서 사용해야 한다면 다음과 같은 우회 방식을 고려해야 합니다.

How to: Set a Thread Name in Native Code
; https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2015/debugger/how-to-set-a-thread-name-in-native-code?view=vs-2015

//  
// Usage: SetThreadName ((DWORD)-1, "MainThread");  
//  
#include <windows.h>  
const DWORD MS_VC_EXCEPTION = 0x406D1388;  
#pragma pack(push,8)  
typedef struct tagTHREADNAME_INFO  
{  
    DWORD dwType; // Must be 0x1000.  
    LPCSTR szName; // Pointer to name (in user addr space).  
    DWORD dwThreadID; // Thread ID (-1=caller thread).  
    DWORD dwFlags; // Reserved for future use, must be zero.  
 } THREADNAME_INFO;  
#pragma pack(pop)  
void SetThreadName(DWORD dwThreadID, const char* threadName) {  
    THREADNAME_INFO info;  
    info.dwType = 0x1000;  
    info.szName = threadName;  
    info.dwThreadID = dwThreadID;  
    info.dwFlags = 0;  
#pragma warning(push)  
#pragma warning(disable: 6320 6322)  
    __try{  
        RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info);  
    }  
    __except (EXCEPTION_EXECUTE_HANDLER){  
    }  
#pragma warning(pop)  
}




그다음, 리눅스 환경이라면 pthread_setname_np 함수를 사용할 수 있습니다.

pthread_setname_np(3)
; https://man7.org/linux/man-pages/man3/pthread_setname_np.3.html

int pthread_setname_np(pthread_t thread, const char *name);

역시나 (C#에서도 p/invoke를 활용해) 다음과 같이 작성할 수 있습니다.

using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;

public enum LinuxSysCall
{
    __NR_gettid = 186,
}

internal class Program
{
    [DllImport("libpthread.so.0")]
    public static extern int pthread_setname_np(ulong thread, string name);

    [DllImport("libpthread.so.0")]
    public static extern int pthread_getname_np(ulong thread, StringBuilder name, ulong len);

    [DllImport("libpthread.so.0")]
    public static extern ulong pthread_self();

    public static void SetThreadName(string name)
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            // ...[생략]...
        }
        else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
        {
            /*
            The thread name is a meaningful C language string, whose length is restricted to 16 characters, 
            including the terminating null byte('\0').
            */
            Debug.Assert(name.Length < 16);

            ulong ptid = pthread_self();
            pthread_setname_np(ptid, name);

#if DEBUG
            StringBuilder sb = new StringBuilder(16);
            pthread_getname_np(ptid, sb, (ulong)sb.Capacity);

            Debug.Assert(name == sb.ToString());
#endif
        }
    }

    static void Main(string[] args)
    {
        SetThreadName("Main Thread2");
    }
}

리눅스의 경우 2가지 정도 유의할 점이 있는데요, 우선 스레드 이름을 (null을 포함해야 하니) 15자 이내로 해야 한다는 점입니다. 만약 16자 이상으로 하면 pthread_setname_np은 34(ERANGE) 에러를 반환합니다.

#include <errno.h> // #include <errno-base.h>

#define ERANGE      34  /* Math result not representable */

그다음 문제는, 위의 예제에서는 pthread_setname_np의 첫 번째 인자를 ulong으로 지정했지만 엄밀히 pthread_t 타입은 단일 정숫값이라고 표준에 정의돼 있지 않아 구현에 따라 달라질 수 있다는 점입니다.

// https://stackoverflow.com/questions/1759794/how-to-print-pthread-t

In this case, it depends on the operating system, since the POSIX standard no longer requires pthread_t to be an arithmetic type:

IEEE Std 1003.1-2001/Cor 2-2004, item XBD/TC2/D6/26 is applied, adding pthread_t to the list of types that are not required to be arithmetic types, thus allowing pthread_t to be defined as a structure.

일단은 Ubuntu의 경우 (Linux의 경우 64비트인) "unsigned long int"로 정의돼 있어,

$ grep pthread_t /usr/include/x86_64-linux-gnu/bits/pthreadtypes.h
typedef unsigned long int pthread_t;

저도 C# p/invoke 코드에서 ulong으로 지정한 것입니다. 그러니, 다른 환경이라면 pthread_t 타입을 확인하고 적절히 수정해야 합니다.

(pthread_setname_np(3) 문서에 저 함수의 접미사인 "_np"는 "nonportable"을 의미한다고 합니다.)

부가적으로 stackoverflow의 덧글에 보면 pthread_t 타입을 포인터로 변환해 그것이 가리키는 int 값이 스레드 ID라는 설명이 있는데요,

int get_tid(pthread_t tid)
{
    assert_fatal(sizeof(int) >= sizeof(pthread_t));

    int * threadid = (int *) (void *) &tid;
    return *threadid;
}

저 때, 혹은 저 운영체제에서는 그랬을 수 있겠지만 제가 테스트한 Ubuntu 20.04에서는 다른 값이 나왔습니다.

ulong ptid = pthread_self();

nint ptr = (nint)ptid;
uint thread_id_from_pthread = (uint)Marshal.ReadInt32(ptr);
uint thread_id_from_gettid = (uint)gettid();

Console.WriteLine($"ptid = {ptid}, from_gettid = {thread_id_from_gettid}, from_pthread = {thread_id_from_pthread}");

// 출력 결과: ptid = 140310287091520, from_gettid = 69171, from_pthread = 2295465792




지금까지 Native 영역의 함수를 호출해 Thread Name을 설정하는 것을 실습했는데요, 재미있는 건 저게 C#의 Thread.CurrentThread.Name에 반영이 안 된다는 문제가 있습니다.

Thread.CurrentThread.Name = "Main Thread";
SetThreadName("Main Thread2");

Debug.Assert(Thread.CurrentThread.Name == "Main Thread2"); // 언제나 assertion 발생

이유가 뭘까요? ^^ 그것은 Thread.Name의 속성 정의를 보면 알 수 있습니다.

// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs#L385

public string? Name
{
    get => _name;
    set
    {
        lock (this)
        {
            if (_name != value)
            {
                _name = value;
                ThreadNameChanged(value);
                _mayNeedResetForThreadPool = true;
            }
        }
    }
}

보는 바와 같이 내부의 _name 필드에 값을 보관해 이후 재사용을 하기 때문에 native 영역의 함수에서 스레드 이름을 설정해도 그것이 반영되지 않게 됩니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




마지막으로, 심심한데 위의 Thread.Name set에 있는 ThreadNameChanged 메서드의 구현을 따라가 볼까요?

// https://github.com/dotnet/runtime/blob/main/src/coreclr/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs#L180

private void ThreadNameChanged(string? value)
{
    InformThreadNameChange(GetNativeHandle(), value, value?.Length ?? 0);
    GC.KeepAlive(this);
}

[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ThreadNative_InformThreadNameChange", StringMarshalling = StringMarshalling.Utf16)]
private static partial void InformThreadNameChange(ThreadHandle t, string? name, int len);

// https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/comsynchronizable.cpp#L747

extern "C" void QCALLTYPE ThreadNative_InformThreadNameChange(QCall::ThreadHandle thread, LPCWSTR name, INT32 len)
{
    QCALL_CONTRACT;

    BEGIN_QCALL;

    Thread* pThread = thread;

    // The name will show up in ETW traces and debuggers which is very helpful if more and more threads
    // get a meaningful name. Will also show up in Linux in gdb and such.
    if (len > 0 && name != NULL && pThread->GetThreadHandle() != INVALID_HANDLE_VALUE)
    {
        SetThreadName(pThread->GetThreadHandle(), name);
    }

#ifdef PROFILING_SUPPORTED
    {
        BEGIN_PROFILER_CALLBACK(CORProfilerTrackThreads());
        if (name == NULL)
        {
            (&g_profControlBlock)->ThreadNameChanged((ThreadID)pThread, 0, NULL);
        }
        else
        {
            (&g_profControlBlock)->ThreadNameChanged((ThreadID)pThread, len, (WCHAR*)name);
        }
        END_PROFILER_CALLBACK();
    }
#endif // PROFILING_SUPPORTED

#ifdef DEBUGGING_SUPPORTED
    if (CORDebuggerAttached())
    {
        _ASSERTE(NULL != g_pDebugInterface);
        g_pDebugInterface->NameChangeEvent(NULL, pThread);
    }
#endif // DEBUGGING_SUPPORTED

    END_QCALL;
}

// https://github.com/dotnet/runtime/blob/main/src/coreclr/utilcode/winfix.cpp#L137

HRESULT SetThreadName(HANDLE hThread, PCWSTR lpThreadDescription)
{
    return SetThreadDescription(hThread, lpThreadDescription);
}

// https://github.com/dotnet/runtime/blob/main/src/coreclr/pal/src/thread/thread.cpp#L1459

HRESULT
PALAPI
SetThreadDescription(
    IN HANDLE hThread,
    IN PCWSTR lpThreadDescription)
{
    PERF_ENTRY(SetThreadDescription);
    ENTRY("SetThreadDescription(hThread=%p,lpThreadDescription=%p)\n", hThread, lpThreadDescription);

    CPalThread *pThread = InternalGetCurrentThread();

    CPalThread *pTargetThread = NULL;
    IPalObject *pobjThread = NULL;
    int nameSize;
    char *nameBuf = NULL;

    PAL_ERROR palError = InternalGetThreadDataFromHandle(pThread, hThread, &pTargetThread, &pobjThread);
    if (palError == NO_ERROR)
    {
        // Ignore requests to set the main thread name because
        // it causes the value returned by Process.ProcessName to change.
        if ((pid_t)pTargetThread->GetThreadId() != getpid())
        {
            nameSize = WideCharToMultiByte(CP_ACP, 0, lpThreadDescription, -1, NULL, 0, NULL, NULL);
            if (nameSize > 0)
            {
                nameBuf = (char *)malloc(nameSize);
                if (nameBuf == NULL || WideCharToMultiByte(CP_ACP, 0, lpThreadDescription, -1, nameBuf, nameSize, NULL, NULL) != nameSize)
                {
                    pThread->SetLastError(ERROR_INSUFFICIENT_BUFFER);
                }
                else
                {
                    int setNameResult = minipal_set_thread_name(pTargetThread->GetPThreadSelf(), nameBuf);
                    (void)setNameResult; // used
                    _ASSERTE(setNameResult == 0);
                }

                free(nameBuf);
            }
            else
            {
                pThread->SetLastError(ERROR_INVALID_PARAMETER);
            }
        }

        if (pobjThread != NULL)
            pobjThread->ReleaseReference(pThread);
    }

    LOGEXIT("SetThreadDescription");
    PERF_EXIT(SetThreadDescription);

    return HRESULT_FROM_WIN32(palError);
}

minipal_set_thread_name의 구현은 운영체제마다 달라지는 데 *NIX 계열은 아래의 위치에 있는 함수로 연결됩니다.

// https://github.com/dotnet/runtime/blob/main/src/native/minipal/thread.h#L87

/**
 * Set the name of the specified thread.
 *
 * @param thread The thread for which to set the name.
 * @param name The desired name for the thread.
 * @return 0 on success, or an error code if the operation fails.
 */
static inline int minipal_set_thread_name(pthread_t thread, const char* name)
{
#ifdef __wasm
    // WASM does not support pthread_setname_np yet: https://github.com/emscripten-core/emscripten/pull/18751
    return 0;
#else
    const char* threadName = name;
    char truncatedName[MINIPAL_MAX_THREAD_NAME_LENGTH + 1];

    if (strlen(name) > MINIPAL_MAX_THREAD_NAME_LENGTH)
    {
        strncpy(truncatedName, name, MINIPAL_MAX_THREAD_NAME_LENGTH);
        truncatedName[MINIPAL_MAX_THREAD_NAME_LENGTH] = '\0';
        threadName = truncatedName;
    }

#if defined(__APPLE__)
    // On Apple OSes, pthread_setname_np only works for the calling thread.
    if (thread != pthread_self()) return 0;

    return pthread_setname_np(threadName);
#elif defined(__HAIKU__)
    return rename_thread(get_pthread_thread_id(thread), threadName);
#else
    return pthread_setname_np(thread, threadName);
#endif
#endif
}

결국, .NET BCL도 내부적으로는 pthread_setname_np를 호출하는군요. ^^




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







[최초 등록일: ]
[최종 수정일: 11/15/2024]

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)
13718정성태8/27/20247411오류 유형: 921. Visual C++ - error C1083: Cannot open include file: 'float.h': No such file or directory [2]
13717정성태8/26/20247019VS.NET IDE: 192. Visual Studio 2022 - Windows XP / 2003용 C/C++ 프로젝트 빌드
13716정성태8/21/20246750C/C++: 167. Visual C++ - 윈도우 환경에서 _execv 동작 [1]
13715정성태8/19/20247348Linux: 78. 리눅스 C/C++ - 특정 버전의 glibc 빌드 (docker-glibc-builder)
13714정성태8/19/20246735닷넷: 2295. C# 12 - 기본 생성자(Primary constructors) (책 오타 수정) [3]
13713정성태8/16/20247444개발 환경 구성: 721. WSL 2에서의 Hyper-V Socket 연동
13712정성태8/14/20247214개발 환경 구성: 720. Synology NAS - docker 원격 제어를 위한 TCP 바인딩 추가
13711정성태8/13/20248055Linux: 77. C# / Linux - zombie process (defunct process) [1]파일 다운로드1
13710정성태8/8/20247974닷넷: 2294. C# 13 - (6) iterator 또는 비동기 메서드에서 ref와 unsafe 사용을 부분적으로 허용파일 다운로드1
13709정성태8/7/20247739닷넷: 2293. C# - safe/unsafe 문맥에 대한 C# 13의 (하위 호환을 깨는) 변화파일 다운로드1
13708정성태8/7/20247526개발 환경 구성: 719. ffmpeg / YoutubeExplode - mp4 동영상 파일로부터 Audio 파일 추출
13707정성태8/6/20247757닷넷: 2292. C# - 자식 프로세스의 출력이 4,096보다 많은 경우 Process.WaitForExit 호출 시 hang 현상파일 다운로드1
13706정성태8/5/20247881개발 환경 구성: 718. Hyper-V - 리눅스 VM에 새로운 디스크 추가
13705정성태8/4/20248145닷넷: 2291. C# 13 - (5) params 인자 타입으로 컬렉션 허용 [2]파일 다운로드1
13704정성태8/2/20248100닷넷: 2290. C# - 간이 dotnet-dump 프로그램 만들기파일 다운로드1
13703정성태8/1/20247429닷넷: 2289. "dotnet-dump ps" 명령어가 닷넷 프로세스를 찾는 방법
13702정성태7/31/20247827닷넷: 2288. Collection 식을 지원하는 사용자 정의 타입을 CollectionBuilder 특성으로 성능 보완파일 다운로드1
13701정성태7/30/20248098닷넷: 2287. C# 13 - (4) Indexer를 이용한 개체 초기화 구문에서 System.Index 연산자 허용파일 다운로드1
13700정성태7/29/20247698디버깅 기술: 200. DLL Export/Import의 Hint 의미
13699정성태7/27/20248226닷넷: 2286. C# 13 - (3) Monitor를 대체할 Lock 타입파일 다운로드1
13698정성태7/27/20248188닷넷: 2285. C# - async 메서드에서의 System.Threading.Lock 잠금 처리파일 다운로드1
13697정성태7/26/20247913닷넷: 2284. C# - async 메서드에서의 lock/Monitor.Enter/Exit 잠금 처리파일 다운로드1
13696정성태7/26/20247439오류 유형: 920. dotnet publish - error NETSDK1047: Assets file '...\obj\project.assets.json' doesn't have a target for '...'
13695정성태7/25/20247423닷넷: 2283. C# - Lock / Wait 상태에서도 STA COM 메서드 호출 처리파일 다운로드1
13694정성태7/25/20247889닷넷: 2282. C# - ASP.NET Core Web App의 Request 용량 상한값 (Kestrel, IIS)
13693정성태7/24/20247225개발 환경 구성: 717. Visual Studio - C# 프로젝트에서 레지스트리에 등록하지 않은 COM 개체 참조 및 사용 방법파일 다운로드1
1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...