Microsoft MVP성태의 닷넷 이야기
닷넷: 2312. C#, C++ - Windows / Linux 환경의 Thread Name 설정 [링크 복사], [링크+제목 복사],
조회: 2396
글쓴 사람
정성태 (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)
13861정성태1/11/2025455Windows: 276. 명령행에서 원격 서비스를 동기/비동기로 시작/중지
13860정성태1/10/2025510디버깅 기술: 216. WinDbg - 2가지 유형의 식 평가 방법(MASM, C++)
13859정성태1/9/2025599디버깅 기술: 215. Windbg - syscall 이후 실행되는 KiSystemCall64 함수 및 SSDT 디버깅
13858정성태1/8/2025658개발 환경 구성: 738. PowerShell - 원격 호출 시 "powershell.exe"가 아닌 "pwsh.exe" 환경으로 명령어를 실행하는 방법
13857정성태1/7/2025752C/C++: 187. Golang - 콘솔 응용 프로그램을 Linux 데몬 서비스를 지원하도록 변경파일 다운로드1
13856정성태1/6/2025815디버깅 기술: 214. Windbg - syscall 단계까지의 Win32 API 호출 (예: Sleep)
13855정성태12/28/20241435오류 유형: 941. Golang - os.StartProcess() 사용 시 오류 정리
13854정성태12/27/20241414C/C++: 186. Golang - 콘솔 응용 프로그램을 NT 서비스를 지원하도록 변경파일 다운로드1
13853정성태12/26/20241582디버깅 기술: 213. Windbg - swapgs 명령어와 (Ring 0 커널 모드의) FS, GS Segment 레지스터
13852정성태12/25/20241596디버깅 기술: 212. Windbg - (Ring 3 사용자 모드의) FS, GS Segment 레지스터파일 다운로드1
13851정성태12/23/20241514디버깅 기술: 211. Windbg - 커널 모드 디버깅 상태에서 사용자 프로그램을 디버깅하는 방법
13850정성태12/23/20241524오류 유형: 940. "Application Information" 서비스를 중지한 경우, "This file does not have an app associated with it for performing this action."
13849정성태12/20/20241558디버깅 기술: 210. Windbg - 논리(가상) 주소를 Segmentation을 거쳐 선형 주소로 변경
13848정성태12/18/20241784디버깅 기술: 209. Windbg로 알아보는 Prototype PTE파일 다운로드2
13847정성태12/18/20241676오류 유형: 939. golang - 빌드 시 "unknown directive: toolchain" 오류 빌드 시 이런 오류가 발생한다면?
13846정성태12/17/20241768디버깅 기술: 208. Windbg로 알아보는 Trans/Soft PTE와 2가지 Page Fault 유형파일 다운로드1
13845정성태12/16/20241958디버깅 기술: 207. Windbg로 알아보는 PTE (_MMPTE)
13844정성태12/14/20241833디버깅 기술: 206. Windbg로 알아보는 PFN (_MMPFN)파일 다운로드1
13843정성태12/13/20241869오류 유형: 938. Docker container 내에서 빌드 시 error MSB3021: Unable to copy file "..." to "...". Access to the path '...' is denied.
13842정성태12/12/20242264디버깅 기술: 205. Windbg - KPCR, KPRCB
13841정성태12/11/20242198오류 유형: 937. error MSB4044: The "ValidateValidArchitecture" task was not given a value for the required parameter "RemoteTarget"
13840정성태12/11/20241998오류 유형: 936. msbuild - Your project file doesn't list 'win' as a "RuntimeIdentifier"
13839정성태12/11/20241972오류 유형: 936. msbuild - error CS1617: Invalid option '12.0' for /langversion. Use '/langversion:?' to list supported values.
13838정성태12/4/20242244오류 유형: 935. Windbg - Breakpoint 0's offset expression evaluation failed.
13837정성태12/3/20242257디버깅 기술: 204. Windbg - 윈도우 핸들 테이블 (3) - Windows 10 이상인 경우
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...