Files
tracy/public/client/TracyThread.hpp
T
Clément Grégoire a181b62a6a Don't deadlock when joining threads from a DLL being unloaded.
When Tracy is built into a DLL, the static `Profiler` is destroyed inside `DllMain(DLL_PROCESS_DETACH)` while the loader lock is held. `Thread::~Thread()` waited for the thread to exit, but a thread can only exit by taking the loader lock for `DLL_THREAD_DETACH`, so `FreeLibrary` hung forever (typically in `s_symbolThread->~Thread()`).

The thread function now signals an event when it is done and parks in an alertable sleep instead of returning. The destructor waits for that event rather than for the thread itself. Inside a loader callout (detected with `RtlIsThreadWithinLoaderCallout`) the parked thread is terminated, which is safe as it holds no locks at that point. Everywhere else it is woken with an APC and exits normally, so `thread_local` destructors still run. If the ntdll export is unavailable, the previous behaviour is kept.

Same approach as `asio::detail::win_thread`.

Reproduced with a test DLL embedding Tracy (`TRACY_ON_DEMAND`) loaded and freed in a loop: `FreeLibrary` hung on the first unload before the change, and 100 cycles pass after it when the DLL stays loaded for at least a few ms. Unloading immediately after loading still hangs if the Tracy threads are blocked on the loader lock during their startup (e.g. in `gethostname` or `SymInitialize`), as they never reach the parking point. Startup and shutdown timings are unchanged.
2026-10-29 14:14:20 +01:00

129 lines
2.8 KiB
C++

#ifndef __TRACYTHREAD_HPP__
#define __TRACYTHREAD_HPP__
#if defined _WIN32
# include <windows.h>
#else
# include <pthread.h>
#endif
#ifdef TRACY_MANUAL_LIFETIME
# include "../common/TracyAlloc.hpp"
#endif
namespace tracy
{
#ifdef TRACY_MANUAL_LIFETIME
extern thread_local bool RpThreadInitDone;
#endif
class ThreadExitHandler
{
public:
~ThreadExitHandler()
{
#ifdef TRACY_MANUAL_LIFETIME
# if defined TRACY_HAS_CUSTOM_ALLOCATOR
PlatformAllocatorThreadFinalize();
# elif defined TRACY_USE_RPMALLOC
rpmalloc_thread_finalize( 1 );
# endif
RpThreadInitDone = false;
#endif
}
};
#if defined _WIN32
class Thread
{
public:
Thread( void(*func)( void* ptr ), void* ptr )
: m_func( func )
, m_ptr( ptr )
, m_exit( CreateEventW( nullptr, TRUE, FALSE, nullptr ) )
, m_hnd( CreateThread( nullptr, 0, Launch, this, 0, nullptr ) )
{}
~Thread()
{
// The thread signals m_exit when it is done and then parks, instead of exiting, so that
// it can be joined from DllMain (e.g. a static Profiler destroyed by FreeLibrary).
// A normal thread exit needs the loader lock, which the joining thread already holds.
HANDLE handles[2] = { m_exit, m_hnd };
WaitForMultipleObjects( 2, handles, FALSE, INFINITE );
CloseHandle( m_exit );
if( InLoaderCallout() )
{
TerminateThread( m_hnd, 0 );
}
else
{
QueueUserAPC( WakeUp, m_hnd, 0 );
WaitForSingleObject( m_hnd, INFINITE );
}
CloseHandle( m_hnd );
}
HANDLE Handle() const { return m_hnd; }
private:
static bool InLoaderCallout()
{
typedef BOOLEAN(NTAPI* IsWithinCallout)();
static const auto fn = (IsWithinCallout)GetProcAddress( GetModuleHandleW( L"ntdll.dll" ), "RtlIsThreadWithinLoaderCallout" );
return fn && fn();
}
static void CALLBACK WakeUp( ULONG_PTR ) {}
static DWORD WINAPI Launch( void* ptr )
{
auto self = (Thread*)ptr;
self->m_func( self->m_ptr );
// *self may be gone as soon as m_exit is signaled.
const HANDLE exit = self->m_exit;
SetEvent( exit );
SleepEx( INFINITE, TRUE );
return 0;
}
void(*m_func)( void* ptr );
void* m_ptr;
HANDLE m_exit;
HANDLE m_hnd;
};
#else
class Thread
{
public:
Thread( void(*func)( void* ptr ), void* ptr )
: m_func( func )
, m_ptr( ptr )
{
pthread_create( &m_thread, nullptr, Launch, this );
}
~Thread()
{
pthread_join( m_thread, nullptr );
}
pthread_t Handle() const { return m_thread; }
private:
static void* Launch( void* ptr ) { ((Thread*)ptr)->m_func( ((Thread*)ptr)->m_ptr ); return nullptr; }
void(*m_func)( void* ptr );
void* m_ptr;
pthread_t m_thread;
};
#endif
}
#endif