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

비밀번호

댓글 작성자
 




... 61  62  63  64  65  66  67  68  [69]  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
12211정성태4/27/202019265개발 환경 구성: 486. WSL에서 Makefile로 공개된 리눅스 환경의 C/C++ 소스 코드 빌드
12210정성태4/20/202020710.NET Framework: 903. .NET Framework의 Strong-named 어셈블리 바인딩 (1) - app.config을 이용한 바인딩 리디렉션 [1]파일 다운로드1
12209정성태4/13/202017414오류 유형: 614. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우 (2)
12208정성태4/12/202015978Linux: 29. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우
12207정성태4/2/202015829스크립트: 19. Windows PowerShell의 NonInteractive 모드
12206정성태4/2/202018445오류 유형: 613. 파일 잠금이 바로 안 풀린다면? - The process cannot access the file '...' because it is being used by another process.
12205정성태4/2/202015107스크립트: 18. Powershell에서는 cmd.exe의 명령어를 지원하진 않습니다.
12204정성태4/1/202015106스크립트: 17. Powershell 명령어에 ';' (semi-colon) 문자가 포함된 경우
12203정성태3/18/202017952오류 유형: 612. warning: 'C:\ProgramData/Git/config' has a dubious owner: '...'.
12202정성태3/18/202021208개발 환경 구성: 486. .NET Framework 프로젝트를 위한 GitLab CI/CD Runner 구성
12201정성태3/18/202018439오류 유형: 611. git-credential-manager.exe: Using credentials for username "Personal Access Token". [1]
12200정성태3/18/202018530VS.NET IDE: 145. NuGet + Github 라이브러리 디버깅 관련 옵션 3가지 - "Enable Just My Code" / "Enable Source Link support" / "Suppress JIT optimization on module load (Managed only)"
12199정성태3/17/202016172오류 유형: 610. C# - CodeDomProvider 사용 시 Unhandled Exception: System.IO.DirectoryNotFoundException: Could not find a part of the path '...\f2_6uod0.tmp'.
12198정성태3/17/202019532오류 유형: 609. SQL 서버 접속 시 "Cannot open user default database. Login failed."
12197정성태3/17/202018830VS.NET IDE: 144. .NET Core 콘솔 응용 프로그램을 배포(publish) 시 docker image 자동 생성 - 두 번째 이야기 [1]
12196정성태3/17/202015950오류 유형: 608. The ServicedComponent being invoked is not correctly configured (Use regsvcs to re-register).
12195정성태3/16/202018268.NET Framework: 902. C# - 프로세스의 모든 핸들을 열람 - 세 번째 이야기
12194정성태3/16/202020999오류 유형: 607. PostgreSQL - Npgsql.NpgsqlException: sorry, too many clients already
12193정성태3/16/202017917개발 환경 구성: 485. docker - SAP Adaptive Server Enterprise 컨테이너 실행 [1]
12192정성태3/14/202019939개발 환경 구성: 484. docker - Sybase Anywhere 16 컨테이너 실행
12191정성태3/14/202021050개발 환경 구성: 483. docker - OracleXE 컨테이너 실행 [1]
12190정성태3/14/202015628오류 유형: 606. Docker Desktop 업그레이드 시 "The process cannot access the file 'C:\Program Files\Docker\Docker\resources\dockerd.exe' because it is being used by another process."
12189정성태3/13/202021232개발 환경 구성: 482. Facebook OAuth 처리 시 상태 정보 전달 방법과 "유효한 OAuth 리디렉션 URI" 설정 규칙
12188정성태3/13/202026023Windows: 169. 부팅 시점에 실행되는 chkdsk 결과를 확인하는 방법
12187정성태3/12/202015600오류 유형: 605. NtpClient was unable to set a manual peer to use as a time source because of duplicate error on '...'.
12186정성태3/12/202017402오류 유형: 604. The SysVol Permissions for one or more GPOs on this domain controller and not in sync with the permissions for the GPOs on the Baseline domain controller.
... 61  62  63  64  65  66  67  68  [69]  70  71  72  73  74  75  ...