Microsoft MVP성태의 닷넷 이야기
닷넷: 2312. C#, C++ - Windows / Linux 환경의 Thread Name 설정 [링크 복사], [링크+제목 복사],
조회: 5679
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 136  137  138  139  140  141  142  143  144  145  [146]  147  148  149  150  ...
NoWriterDateCnt.TitleFile(s)
1403정성태1/14/201331977.NET Framework: 357. .NET 4.5의 2GB 힙 한계 극복
1402정성태1/14/201332549오류 유형: 166. SmtpClient.Send 오류 - net_io_connectionclosed
1401정성태1/11/201329874.NET Framework: 356. (공개키를 담은) 자바의 key 파일을 닷넷의 RSACryptoServiceProvider에서 사용하는 방법 [2]파일 다운로드1
1400정성태1/10/201329014Windows: 69. 작업표시줄의 터치 키보드(Touch Keyboard) 없애는 방법 [3]
1399정성태1/9/201324656.NET Framework: 355. 닷넷 환경이 왜 C/C++보다 느릴까요? [8]
1398정성태1/8/201325114오류 유형: 165. 새로 설치한 Visual Studio 2010의 Team Explorer 실행시 비정상 종료가 된다면?
1397정성태1/3/201328606Windows: 68. 윈도우 설치 ISO 이미지를 USB 하드에 적용하는 방법 [2]
1396정성태12/27/201229813사물인터넷: 2. 넷두이노 - 4.2.0 펌웨어 업데이트 방법 [1]파일 다운로드1
1395정성태12/26/201220674.NET Framework: 354. x64 - AspCompat과 STA COM 개체가 성능에 미치는 영향
1394정성태12/25/201222125.NET Framework: 353. x86 - AspCompat과 STA COM 개체가 성능에 미치는 영향
1393정성태12/25/201222482.NET Framework: 352. x64에서 필수로 지정하도록 바뀐 STAThread 특성 [2]
1392정성태12/21/201232499사물인터넷: 1. .NET Micro Framework - 넷두이노 플러스 [7]
1391정성태12/21/201225886.NET Framework: 351. JavaScriptSerializer, DataContractJsonSerializer, Json.NET [3]파일 다운로드1
1390정성태12/20/201223958.NET Framework: 350. String 데이터를 Stream으로 변환하는 방법 [2]
1389정성태12/12/201222283.NET Framework: 349. .NET Thread 인스턴스로부터 COM Apartment 유형 확인하는 방법파일 다운로드1
1388정성태12/12/201223339.NET Framework: 348. .NET x64 응용 프로그램에서 Teb 주소를 구하는 방법파일 다운로드1
1387정성태12/12/201228263VC++: 64. x64 Visual C++에서 TEB 주소 구하는 방법
1386정성태12/12/201229977디버깅 기술: 53. windbg - 덤프 파일로부터 네이티브 DLL을 추출하는 방법 [1]
1385정성태12/12/201225041디버깅 기술: 52. Windbg - The version of SOS does not match the version of CLR you are debugging.
1384정성태12/12/201229865개발 환경 구성: 178. System32 폴더의 64비트 DLL을 32비트 Depends.exe에서 보는 방법
1383정성태12/10/201225774개발 환경 구성: 177. 기업용 메신저를 위한 Office Communicator Server 2007 설치 [1]
1382정성태12/8/201228636개발 환경 구성: 176. WebPagetest 서버 - 설치 및 테스트
1381정성태12/5/201227143.NET Framework: 347. C# - 프로세스(EXE) 수준의 Singleton 개체 생성 [2]파일 다운로드1
1380정성태11/28/201237177.NET Framework: 346. 닷넷 개발자에게 Node.js의 의미 [17]
1379정성태11/26/201230317.NET Framework: 345. C# 부호(+, -)에 대한 비트 변환
1378정성태11/22/201231637Java: 14. 안드로이드 - Hello World 실습 [7]
... 136  137  138  139  140  141  142  143  144  145  [146]  147  148  149  150  ...