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를 호출하는군요. ^^
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]