Skip to content

Commit 3c6f279

Browse files
committed
Lazily create the cleanup thread only when we encounter the scenario that would deadlock if cleanup were on the finalizer thread.
Add tests for this specific scenario.
1 parent c0a7e8a commit 3c6f279

File tree

19 files changed

+389
-17
lines changed

19 files changed

+389
-17
lines changed

src/coreclr/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,11 +441,26 @@ private SafeWaitHandle GetJoinHandle()
441441
private volatile ManualResetEvent? _joinEvent;
442442
private SafeWaitHandle GetJoinHandle()
443443
{
444-
ManualResetEvent joinEvent = Interlocked.CompareExchange(ref _joinEvent, new ManualResetEvent(false), null)!;
444+
ManualResetEvent newEvent = new ManualResetEvent(false);
445+
ManualResetEvent joinEvent = Interlocked.CompareExchange(ref _joinEvent, newEvent, null) ?? newEvent;
445446
return joinEvent.SafeWaitHandle;
446447
}
447448
#endif
448449

450+
#if TARGET_UNIX || TARGET_BROWSER || TARGET_WASI
451+
static partial void EnsureDetachedThreadCleanupThreadExistsCore()
452+
{
453+
if (!EnsureDetachedThreadCleanupThreadExistsInternal())
454+
{
455+
throw new OutOfMemoryException();
456+
}
457+
}
458+
459+
[LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ThreadNative_EnsureDetachedThreadCleanupThreadExists")]
460+
[return: MarshalAs(UnmanagedType.Bool)]
461+
private static partial bool EnsureDetachedThreadCleanupThreadExistsInternal();
462+
#endif // TARGET_UNIX || TARGET_BROWSER || TARGET_WASI
463+
449464
/// <summary>
450465
/// Max value to be passed into <see cref="SpinWait(int)"/> for optimal delaying. This value is normalized to be
451466
/// appropriate for the processor.
@@ -563,6 +578,7 @@ private void OnThreadExiting()
563578
// Inform the wait subsystem that the thread is exiting. For instance, this would abandon any mutexes locked by
564579
// the thread.
565580
_waitInfo?.OnThreadExiting();
581+
SetJoinHandle();
566582
#endif
567583
}
568584

src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Unix.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ private static void OnThreadExit()
5353
// the thread.
5454
currentThread._waitInfo.OnThreadExiting();
5555
StopThread(currentThread);
56-
currentThread._stopped.Set();
56+
SetJoinHandle(currentThread);
5757
}
5858
}
5959

src/coreclr/vm/ceemain.cpp

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -936,10 +936,6 @@ void EEStartupHelper()
936936
FinalizerThread::FinalizerThreadCreate();
937937
#endif // TARGET_WINDOWS
938938

939-
// Start the thread that will clean up managed resources for any non-.NET created threads
940-
// that exit.
941-
ThreadCleanupThread::EnsureCleanupThreadExists();
942-
943939
#ifdef FEATURE_PERFTRACING
944940
// Finish setting up rest of EventPipe - specifically enable SampleProfiler if it was requested at startup.
945941
// SampleProfiler needs to cooperate with the GC which hasn't fully finished setting up in the first part of the

src/coreclr/vm/comsynchronizable.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,20 @@ extern "C" void QCALLTYPE ThreadNative_SpinWait(INT32 iterations)
723723
YieldProcessorNormalized(iterations);
724724
}
725725

726+
extern "C" BOOL QCALLTYPE ThreadNative_EnsureDetachedThreadCleanupThreadExists()
727+
{
728+
QCALL_CONTRACT;
729+
730+
BOOL ret = FALSE;
731+
BEGIN_QCALL;
732+
733+
ret = ThreadCleanupThread::EnsureCleanupThreadExists();
734+
735+
END_QCALL;
736+
737+
return ret;
738+
}
739+
726740
#ifdef TARGET_WINDOWS
727741
// This service can be called on unstarted and dead threads. For unstarted ones, the
728742
// next wait will be interrupted. For dead ones, this service quietly does nothing.

src/coreclr/vm/comsynchronizable.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ extern "C" INT32 QCALLTYPE ThreadNative_GetThreadState(QCall::ThreadHandle threa
5959
extern "C" void QCALLTYPE ThreadNative_SetWaitSleepJoinState(QCall::ThreadHandle thread);
6060
extern "C" void QCALLTYPE ThreadNative_ClearWaitSleepJoinState(QCall::ThreadHandle thread);
6161
extern "C" INT32 QCALLTYPE ThreadNative_ReentrantWaitAny(BOOL alertable, INT32 timeout, INT32 count, HANDLE *handles);
62+
extern "C" BOOL QCALLTYPE ThreadNative_EnsureDetachedThreadCleanupThreadExists();
6263
#ifdef TARGET_WINDOWS
6364
extern "C" void QCALLTYPE ThreadNative_Interrupt(QCall::ThreadHandle thread);
6465
extern "C" void QCALLTYPE ThreadNative_CheckForPendingInterrupt(QCall::ThreadHandle thread);

src/coreclr/vm/finalizerthread.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ static void DoExtraWorkForFinalizer(Thread* finalizerThread)
146146
SystemDomain::System()->ProcessDelayedUnloadLoaderAllocators();
147147
}
148148

149+
if (Thread::m_DetachCount > 0 && !ThreadCleanupThread::UsingExternalCleanupThread())
150+
{
151+
Thread::CleanupDetachedThreads();
152+
}
153+
149154
if (Thread::CleanupNeededForFinalizedThread())
150155
{
151156
Thread::CleanupFinalizedThreads();

src/coreclr/vm/object.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,7 @@ class ThreadBaseObject : public Object
12131213
OBJECTREF m_StartHelper;
12141214
#ifdef TARGET_UNIX
12151215
OBJECTREF m_WaitInfo;
1216+
OBJECTREF m_joinEvent;
12161217
#endif // TARGET_UNIX
12171218

12181219
// The next field (m_InternalThread) is declared as IntPtr in the managed

src/coreclr/vm/qcallentrypoints.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ static const Entry s_QCall[] =
296296
DllImportEntry(ThreadNative_Abort)
297297
DllImportEntry(ThreadNative_ResetAbort)
298298
DllImportEntry(ThreadNative_SpinWait)
299+
DllImportEntry(ThreadNative_EnsureDetachedThreadCleanupThreadExists)
299300
#ifdef TARGET_WINDOWS
300301
DllImportEntry(ThreadNative_CheckForPendingInterrupt)
301302
DllImportEntry(ThreadNative_Interrupt)

src/coreclr/vm/threads.cpp

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6989,6 +6989,9 @@ namespace
69896989
}
69906990
CONTRACTL_END;
69916991

6992+
// If there's no threads to detach right now,
6993+
// go to sleep.
6994+
if (Thread::m_DetachCount == 0)
69926995
{
69936996
GCX_PREEMP();
69946997

@@ -7024,44 +7027,83 @@ namespace
70247027
}
70257028
}
70267029

7027-
void
7030+
BOOL
70287031
ThreadCleanupThread::EnsureCleanupThreadExists()
70297032
{
70307033
CONTRACTL{
70317034
THROWS;
70327035
GC_TRIGGERS;
7033-
MODE_ANY;
7036+
MODE_PREEMPTIVE;
70347037
} CONTRACTL_END;
70357038

7036-
ForeignThreadsToCleanUpEvent = new CLREvent();
7037-
ForeignThreadsToCleanUpEvent->CreateAutoEvent(FALSE);
7039+
if (ForeignThreadsToCleanUpEvent != nullptr)
7040+
{
7041+
// Another thread created the event first.
7042+
// Set the event in case another thread just signaled the finalizer
7043+
// right before we were called.
7044+
ForeignThreadsToCleanUpEvent->Set();
7045+
return TRUE;
7046+
}
7047+
7048+
NewHolder<CLREvent> tempEvent = new CLREvent();
7049+
// We may have threads waiting to clean up already, so create the event in the signaled state.
7050+
tempEvent->CreateAutoEvent(TRUE);
7051+
7052+
if (InterlockedCompareExchangeT(&ForeignThreadsToCleanUpEvent, tempEvent.GetValue(), nullptr) != nullptr)
7053+
{
7054+
// Another thread created the event first, just return.
7055+
return TRUE;
7056+
}
7057+
7058+
tempEvent.SuppressRelease();
70387059

7039-
Thread* pCleanupThread = SetupUnstartedThread();
7060+
Holder<Thread*,DoNothing<Thread*>,DeleteThread> pCleanupThread(SetupUnstartedThread());
70407061

7041-
if (pCleanupThread->CreateNewThread(0, &ForeignThreadCleanupThreadStart, pCleanupThread, W(".NET External Thread Cleanup Thread")))
7062+
// Don't block process shutdown on the cleanup thread.
7063+
pCleanupThread.GetValue()->SetBackground(TRUE);
7064+
7065+
if (pCleanupThread.GetValue()->CreateNewThread(0, &ForeignThreadCleanupThreadStart, pCleanupThread.GetValue(), W(".NET Detached Cleanup Thread")))
70427066
{
7043-
DWORD dwRet = pCleanupThread->StartThread();
7067+
DWORD dwRet = pCleanupThread.GetValue()->StartThread();
70447068

70457069
// When running under a user mode native debugger there is a race
70467070
// between the moment we've created the thread (in CreateNewThread) and
70477071
// the moment we resume it (in StartThread); the debugger may receive
70487072
// the "ct" (create thread) notification, and it will attempt to
70497073
// suspend/resume all threads in the process. Now imagine the debugger
70507074
// resumes this thread first, and only later does it try to resume the
7051-
// newly created thread (the finalizer thread). In these conditions our
7075+
// newly created thread. In these conditions our
70527076
// call to ResumeThread may come before the debugger's call to ResumeThread
70537077
// actually causing dwRet to equal 2.
70547078
// We cannot use IsDebuggerPresent() in the condition below because the
70557079
// debugger may have been detached between the time it got the notification
70567080
// and the moment we execute the test below.
70577081
_ASSERTE(dwRet == 1 || dwRet == 2);
7082+
pCleanupThread.SuppressRelease();
7083+
return TRUE;
70587084
}
7085+
7086+
return FALSE;
70597087
}
70607088
void
70617089
ThreadCleanupThread::SetHasThreadsToCleanUp()
70627090
{
7063-
_ASSERT(ForeignThreadsToCleanUpEvent != nullptr);
7064-
ForeignThreadsToCleanUpEvent->Set();
7091+
// If we don't have a dedicated cleanup thread,
7092+
// we will cleanup detached threads on the finalizer thread.
7093+
if (ForeignThreadsToCleanUpEvent == nullptr)
7094+
{
7095+
FinalizerThread::EnableFinalization();
7096+
}
7097+
else
7098+
{
7099+
ForeignThreadsToCleanUpEvent->Set();
7100+
}
7101+
}
7102+
7103+
bool
7104+
ThreadCleanupThread::UsingExternalCleanupThread()
7105+
{
7106+
return ForeignThreadsToCleanUpEvent != nullptr;
70657107
}
70667108
#endif // DACCESS_COMPILE
70677109

src/coreclr/vm/threads.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5486,7 +5486,8 @@ class StackWalkerWalkingThreadHolder
54865486
class ThreadCleanupThread
54875487
{
54885488
public:
5489-
static void EnsureCleanupThreadExists();
5489+
static bool UsingExternalCleanupThread();
5490+
static BOOL EnsureCleanupThreadExists();
54905491
static void SetHasThreadsToCleanUp();
54915492
};
54925493

0 commit comments

Comments
 (0)