Microsoft MVP성태의 닷넷 이야기
닷넷: 2312. C#, C++ - Windows / Linux 환경의 Thread Name 설정 [링크 복사], [링크+제목 복사],
조회: 5378
글쓴 사람
정성태 (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)
13743정성태9/26/20246410닷넷: 2298. C# - Console 프로젝트에서의 await 대상으로 Main 스레드 활용하는 방법 [1]
13742정성태9/26/20246701닷넷: 2297. C# - ssh-keygen으로 생성한 ecdsa 유형의 Public Key 파일 해석 [1]파일 다운로드1
13741정성태9/25/20245882디버깅 기술: 202. windbg - ASP.NET MVC Web Application (.NET Framework) 응용 프로그램의 덤프 분석 시 요령
13740정성태9/24/20245740기타: 86. RSA 공개키 등의 modulus 값에 0x00 선행 바이트가 있는 이유(ASN.1 인코딩)
13739정성태9/24/20245885닷넷: 2297. C# - ssh-keygen으로 생성한 Public Key 파일 해석과 fingerprint 값(md5, sha256) 생성 [1]파일 다운로드1
13738정성태9/22/20245613C/C++: 174. C/C++ - 윈도우 운영체제에서의 file descriptor, FILE*파일 다운로드1
13737정성태9/21/20245972개발 환경 구성: 727. Visual C++ - 리눅스 프로젝트를 위한 빌드 서버의 msbuild 구성
13736정성태9/20/20245970오류 유형: 923. Visual Studio Code - Could not establish connection to "...": Port forwarding is disabled.
13735정성태9/20/20246046개발 환경 구성: 726. ARM 플랫폼용 Visual C++ 리눅스 프로젝트 빌드
13734정성태9/19/20245748개발 환경 구성: 725. ssh를 이용한 원격 docker 서비스 사용
13733정성태9/19/20246085VS.NET IDE: 194. Visual Studio - Cross Platform / "Authentication Type: Private Key"로 접속하는 방법
13732정성태9/17/20246120개발 환경 구성: 724. ARM + docker 환경에서 .NET 8 설치
13731정성태9/15/20246711개발 환경 구성: 723. C# / Visual C++ - Control Flow Guard (CFG) 활성화 [1]파일 다운로드2
13730정성태9/10/20246371오류 유형: 922. docker - RULE_APPEND failed (No such file or directory): rule in chain DOCKER
13729정성태9/9/20247118C/C++: 173. Windows / C++ - AllocConsole로 할당한 콘솔과 CRT 함수 연동 [1]파일 다운로드1
13728정성태9/7/20246928C/C++: 172. Windows - C 런타임에서 STARTUPINFO의 cbReserved2, lpReserved2 멤버를 사용하는 이유파일 다운로드1
13727정성태9/6/20247467개발 환경 구성: 722. ARM 플랫폼 빌드를 위한 미니 PC(?) - Khadas VIM4 [1]
13726정성태9/5/20247381C/C++: 171. C/C++ - 윈도우 운영체제에서의 file descriptor와 HANDLE파일 다운로드1
13725정성태9/4/20246138디버깅 기술: 201. WinDbg - sos threads 명령어 실행 시 "Failed to request ThreadStore"
13724정성태9/3/20247996닷넷: 2296. Win32/C# - 자식 프로세스로 HANDLE 상속파일 다운로드1
13723정성태9/2/20248254C/C++: 170. Windows - STARTUPINFO의 cbReserved2, lpReserved2 멤버 사용자 정의파일 다운로드2
13722정성태9/2/20245988C/C++: 169. C/C++ - CRT(C Runtime) 함수에 의존성이 없는 프로젝트 생성
13721정성태8/30/20246022C/C++: 168. Visual C++ CRT(C Runtime DLL: msvcr...dll)에 대한 의존성 제거 - 두 번째 이야기
13720정성태8/29/20246189VS.NET IDE: 193. C# - Visual Studio의 자식 프로세스 디버깅
13719정성태8/28/20246331Linux: 79. C++ - pthread_mutexattr_destroy가 없다면 메모리 누수가 발생할까요?
13718정성태8/27/20247413오류 유형: 921. Visual C++ - error C1083: Cannot open include file: 'float.h': No such file or directory [2]
1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...