diff --git a/src/coreclr/nativeaot/Runtime/thread.cpp b/src/coreclr/nativeaot/Runtime/thread.cpp index 45b88d1cde2def..7a1391f75b7b11 100644 --- a/src/coreclr/nativeaot/Runtime/thread.cpp +++ b/src/coreclr/nativeaot/Runtime/thread.cpp @@ -1160,6 +1160,23 @@ bool Thread::CheckPendingRedirect(PCODE eip) #endif // TARGET_X86 +void Thread::SetInterrupted(bool isInterrupted) +{ + if (isInterrupted) + { + SetState(TSF_Interrupted); + } + else + { + ClearState(TSF_Interrupted); + } +} + +bool Thread::CheckInterrupted() +{ + return IsStateSet(TSF_Interrupted); +} + #endif // !DACCESS_COMPILE void Thread::ValidateExInfoStack() @@ -1323,6 +1340,43 @@ FCIMPL0(size_t, RhGetDefaultStackSize) } FCIMPLEND +#ifdef TARGET_WINDOWS +// Native APC callback for Thread.Interrupt +// This callback sets the interrupt flag on the current thread +static VOID CALLBACK InterruptApcCallback(ULONG_PTR /* parameter */) +{ + Thread* pCurrentThread = ThreadStore::RawGetCurrentThread(); + if (!pCurrentThread->IsInitialized()) + { + // If the thread was interrupted before it was started + // the thread won't have been initialized. + // Attach the thread here if it's the first time we're seeing it. + ThreadStore::AttachCurrentThread(); + } + + pCurrentThread->SetInterrupted(true); +} + +// Function to get the address of the interrupt APC callback +FCIMPL0(void*, RhGetInterruptApcCallback) +{ + return (void*)InterruptApcCallback; +} +FCIMPLEND + +FCIMPL0(FC_BOOL_RET, RhCheckAndClearPendingInterrupt) +{ + Thread* pCurrentThread = ThreadStore::RawGetCurrentThread(); + if (pCurrentThread->CheckInterrupted()) + { + pCurrentThread->SetInterrupted(false); + FC_RETURN_BOOL(true); + } + FC_RETURN_BOOL(false); +} +FCIMPLEND +#endif // TARGET_WINDOWS + // Standard calling convention variant and actual implementation for RhpReversePInvokeAttachOrTrapThread EXTERN_C NOINLINE void FASTCALL RhpReversePInvokeAttachOrTrapThread2(ReversePInvokeFrame* pFrame) { diff --git a/src/coreclr/nativeaot/Runtime/thread.h b/src/coreclr/nativeaot/Runtime/thread.h index 83249cfc6bc77e..a4ca462d78d9c9 100644 --- a/src/coreclr/nativeaot/Runtime/thread.h +++ b/src/coreclr/nativeaot/Runtime/thread.h @@ -132,7 +132,7 @@ struct ee_alloc_context struct RuntimeThreadLocals { - ee_alloc_context m_eeAllocContext; + ee_alloc_context m_eeAllocContext; uint32_t volatile m_ThreadStateFlags; // see Thread::ThreadStateFlags enum PInvokeTransitionFrame* m_pTransitionFrame; PInvokeTransitionFrame* m_pDeferredTransitionFrame; // see Thread::EnablePreemptiveMode @@ -214,6 +214,7 @@ class Thread : private RuntimeThreadLocals // // On Unix this is an optimization to not queue up more signals when one is // still being processed. + TSF_Interrupted = 0x00000200, // Set to indicate Thread.Interrupt() has been called on this thread }; private: @@ -390,6 +391,9 @@ class Thread : private RuntimeThreadLocals void SetPendingRedirect(PCODE eip); bool CheckPendingRedirect(PCODE eip); #endif + + void SetInterrupted(bool isInterrupted); + bool CheckInterrupted(); }; #ifndef __GCENV_BASE_INCLUDED__ diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.cs index 0a37079dfa0e9d..5375713f18770c 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.cs @@ -607,6 +607,14 @@ internal static IntPtr RhGetModuleSection(TypeManagerHandle module, ReadyToRunSe [RuntimeImport(RuntimeLibrary, "RhGetDefaultStackSize")] internal static extern unsafe IntPtr RhGetDefaultStackSize(); + [MethodImplAttribute(MethodImplOptions.InternalCall)] + [RuntimeImport(RuntimeLibrary, "RhGetInterruptApcCallback")] + internal static extern unsafe delegate* unmanaged RhGetInterruptApcCallback(); + + [MethodImplAttribute(MethodImplOptions.InternalCall)] + [RuntimeImport(RuntimeLibrary, "RhCheckAndClearPendingInterrupt")] + internal static extern bool RhCheckAndClearPendingInterrupt(); + [MethodImplAttribute(MethodImplOptions.InternalCall)] [RuntimeImport("*", "RhGetCurrentThunkContext")] internal static extern IntPtr GetCurrentInteropThunkContext(); diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index f01fe0f86f38f7..37652e3030b642 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -26,6 +26,59 @@ public sealed partial class Thread partial void PlatformSpecificInitialize(); + internal static void SleepInternal(int millisecondsTimeout) + { + Debug.Assert(millisecondsTimeout >= Timeout.Infinite); + + CheckForPendingInterrupt(); + + Thread currentThread = CurrentThread; + if (millisecondsTimeout == Timeout.Infinite) + { + // Infinite wait - use alertable wait + currentThread.SetWaitSleepJoinState(); + uint result; + while (true) + { + result = Interop.Kernel32.SleepEx(Timeout.UnsignedInfinite, true); + if (result != Interop.Kernel32.WAIT_IO_COMPLETION) + { + break; + } + CheckForPendingInterrupt(); + } + + currentThread.ClearWaitSleepJoinState(); + } + else + { + // Timed wait - use alertable wait + currentThread.SetWaitSleepJoinState(); + long startTime = Environment.TickCount64; + while (true) + { + uint result = Interop.Kernel32.SleepEx((uint)millisecondsTimeout, true); + if (result != Interop.Kernel32.WAIT_IO_COMPLETION) + { + break; + } + // Check if this was our interrupt APC + CheckForPendingInterrupt(); + // Handle APC completion by adjusting timeout and retrying + long currentTime = Environment.TickCount64; + long elapsed = currentTime - startTime; + if (elapsed >= millisecondsTimeout) + { + break; + } + millisecondsTimeout -= (int)elapsed; + startTime = currentTime; + } + + currentThread.ClearWaitSleepJoinState(); + } + } + // Platform-specific initialization of foreign threads, i.e. threads not created by Thread.Start private void PlatformSpecificInitializeExistingThread() { @@ -154,18 +207,57 @@ private bool JoinInternal(int millisecondsTimeout) try { - int result; - if (millisecondsTimeout == 0) { - result = (int)Interop.Kernel32.WaitForSingleObject(waitHandle.DangerousGetHandle(), 0); + int result = (int)Interop.Kernel32.WaitForSingleObject(waitHandle.DangerousGetHandle(), 0); + return result == (int)Interop.Kernel32.WAIT_OBJECT_0; } else { - result = WaitHandle.WaitOneCore(waitHandle.DangerousGetHandle(), millisecondsTimeout, useTrivialWaits: false); + Thread currentThread = CurrentThread; + currentThread.SetWaitSleepJoinState(); + uint result; + if (millisecondsTimeout == Timeout.Infinite) + { + // Infinite wait + while (true) + { + result = Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(), Timeout.UnsignedInfinite, Interop.BOOL.TRUE); + if (result != Interop.Kernel32.WAIT_IO_COMPLETION) + { + break; + } + // Check if this was our interrupt APC + CheckForPendingInterrupt(); + } + } + else + { + long startTime = Environment.TickCount64; + while (true) + { + result = Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(), (uint)millisecondsTimeout, Interop.BOOL.TRUE); + if (result != Interop.Kernel32.WAIT_IO_COMPLETION) + { + break; + } + // Check if this was our interrupt APC + CheckForPendingInterrupt(); + // Handle APC completion by adjusting timeout and retrying + long currentTime = Environment.TickCount64; + long elapsed = currentTime - startTime; + if (elapsed >= millisecondsTimeout) + { + result = Interop.Kernel32.WAIT_TIMEOUT; + break; + } + millisecondsTimeout -= (int)elapsed; + startTime = currentTime; + } + } + currentThread.ClearWaitSleepJoinState(); + return result == (int)Interop.Kernel32.WAIT_OBJECT_0; } - - return result == (int)Interop.Kernel32.WAIT_OBJECT_0; } finally { @@ -212,6 +304,13 @@ private unsafe bool CreateThread(GCHandle thisThreadHandle) // CoreCLR ignores OS errors while setting the priority, so do we SetPriorityLive(_priority); + // If the thread was interrupted before it was started, queue the interruption now + if (GetThreadStateBit(Interrupted)) + { + ClearThreadStateBit(Interrupted); + Interrupt(); + } + Interop.Kernel32.ResumeThread(_osHandle); return true; } @@ -393,7 +492,39 @@ internal static Thread EnsureThreadPoolThreadInitialized() return InitializeExistingThreadPoolThread(); } - public void Interrupt() { throw new PlatformNotSupportedException(); } + public void Interrupt() + { + using (_lock.EnterScope()) + { + // Thread.Interrupt for dead thread should do nothing + if (IsDead()) + { + return; + } + + // Thread.Interrupt for thread that has not been started yet should queue a pending interrupt + // for when we actually create the thread. + if (_osHandle?.IsInvalid ?? true) + { + SetThreadStateBit(Interrupted); + return; + } + + unsafe + { + Interop.Kernel32.QueueUserAPC(RuntimeImports.RhGetInterruptApcCallback(), _osHandle, 0); + } + } + } + + internal static void CheckForPendingInterrupt() + { + if (RuntimeImports.RhCheckAndClearPendingInterrupt()) + { + CurrentThread.ClearWaitSleepJoinState(); + throw new ThreadInterruptedException(); + } + } internal static bool ReentrantWaitsEnabled => GetCurrentApartmentType() == ApartmentType.STA; diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs index 3758fca9e8a0a2..34db94ad986451 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs @@ -17,6 +17,9 @@ public sealed partial class Thread { // Extra bits used in _threadState private const ThreadState ThreadPoolThread = (ThreadState)0x1000; +#if TARGET_WINDOWS + private const ThreadState Interrupted = (ThreadState)0x2000; +#endif // Bits of _threadState that are returned by the ThreadState property private const ThreadState PublicThreadStateMask = (ThreadState)0x1FF; diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.Threading.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.Threading.cs index b323cacc6bcaee..8c6b2169146797 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.Threading.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.Threading.cs @@ -78,5 +78,12 @@ internal enum ThreadPriority : int [LibraryImport(Libraries.Kernel32, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool GetThreadIOPendingFlag(nint hThread, out BOOL lpIOIsPending); + + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static unsafe partial bool QueueUserAPC(delegate* unmanaged pfnAPC, SafeHandle hThread, nuint dwData); + + [LibraryImport(Libraries.Kernel32)] + internal static partial uint SleepEx(uint dwMilliseconds, [MarshalAs(UnmanagedType.Bool)] bool bAlertable); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.Windows.cs index b9b9a0e16f9289..3d16397d7301fb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.Windows.cs @@ -18,7 +18,7 @@ public sealed partial class Thread { internal static void UninterruptibleSleep0() => Interop.Kernel32.Sleep(0); -#if !CORECLR +#if MONO private static void SleepInternal(int millisecondsTimeout) { Debug.Assert(millisecondsTimeout >= -1); diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs index 26ab6b4278b2bf..755ea77ba1cbec 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs @@ -56,6 +56,9 @@ private static unsafe int WaitForMultipleObjectsIgnoringSyncContext(IntPtr* pHan } int result; + + Thread.CheckForPendingInterrupt(); + while (true) { #if NATIVEAOT @@ -75,8 +78,10 @@ private static unsafe int WaitForMultipleObjectsIgnoringSyncContext(IntPtr* pHan if (result != Interop.Kernel32.WAIT_IO_COMPLETION) break; + Thread.CheckForPendingInterrupt(); + // Handle APC completion by adjusting timeout and retrying - if (millisecondsTimeout != -1) + if (millisecondsTimeout != Timeout.Infinite) { long currentTime = Environment.TickCount64; long elapsed = currentTime - startTime; @@ -89,6 +94,7 @@ private static unsafe int WaitForMultipleObjectsIgnoringSyncContext(IntPtr* pHan startTime = currentTime; } } + currentThread.ClearWaitSleepJoinState(); if (result == Interop.Kernel32.WAIT_FAILED) @@ -134,12 +140,16 @@ private static int SignalAndWaitCore(IntPtr handleToSignal, IntPtr handleToWaitO startTime = Environment.TickCount64; } + Thread.CheckForPendingInterrupt(); + // Signal the object and wait for the first time int ret = (int)Interop.Kernel32.SignalObjectAndWait(handleToSignal, handleToWaitOn, (uint)millisecondsTimeout, Interop.BOOL.TRUE); // Handle APC completion by retrying with WaitForSingleObjectEx (without signaling again) while (ret == Interop.Kernel32.WAIT_IO_COMPLETION) { + Thread.CheckForPendingInterrupt(); + if (millisecondsTimeout != -1) { long currentTime = Environment.TickCount64; diff --git a/src/libraries/System.Threading.Thread/tests/ThreadTests.cs b/src/libraries/System.Threading.Thread/tests/ThreadTests.cs index 908d690e7e207d..972056094a5fff 100644 --- a/src/libraries/System.Threading.Thread/tests/ThreadTests.cs +++ b/src/libraries/System.Threading.Thread/tests/ThreadTests.cs @@ -749,7 +749,7 @@ public static void AbortSuspendTest() verify(); e.Set(); - waitForThread(); + waitForThread(); } private static void VerifyLocalDataSlot(LocalDataStoreSlot slot) @@ -916,7 +916,6 @@ public static void LocalDataSlotTest() [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] [ActiveIssue("https://github.com/dotnet/runtime/issues/49521", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] - [ActiveIssue("https://github.com/dotnet/runtime/issues/69919", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))] public static void InterruptTest() { // Interrupting a thread that is not blocked does not do anything, but once the thread starts blocking, it gets @@ -966,7 +965,6 @@ public static void InterruptTest() } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/69919", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))] [ActiveIssue("https://github.com/dotnet/runtime/issues/49521", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public static void InterruptInFinallyBlockTest_SkipOnDesktopFramework() { diff --git a/src/libraries/System.Threading/tests/MonitorTests.cs b/src/libraries/System.Threading/tests/MonitorTests.cs index 13cb675cef34ae..1153330d0ec7e1 100644 --- a/src/libraries/System.Threading/tests/MonitorTests.cs +++ b/src/libraries/System.Threading/tests/MonitorTests.cs @@ -491,7 +491,6 @@ public static void ObjectHeaderSyncBlockTransitionTryEnterRaceTest() [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] [ActiveIssue("https://github.com/dotnet/runtime/issues/49521", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] [ActiveIssue("https://github.com/dotnet/runtime/issues/87718", TestRuntimes.Mono)] - [ActiveIssue("https://github.com/dotnet/runtimelab/issues/155", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))] public static void InterruptWaitTest() { object obj = new(); diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs index 5715dd23923f6b..c0c804db8b9998 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs @@ -364,5 +364,11 @@ internal bool HasExternalEventLoop external_eventloop = value; } } + +#if TARGET_WINDOWS + internal static void CheckForPendingInterrupt() + { + } +#endif } } diff --git a/src/tests/baseservices/threading/regressions/115178/115178.cs b/src/tests/baseservices/threading/regressions/115178/115178.cs index 0367f81c70df70..0a019821189361 100644 --- a/src/tests/baseservices/threading/regressions/115178/115178.cs +++ b/src/tests/baseservices/threading/regressions/115178/115178.cs @@ -288,12 +288,7 @@ public static int TestEntryPoint() { RunTestUsingInfiniteWait(); RunTestUsingTimedWait(); - - // Thread.Interrupt is not implemented on NativeAOT - https://github.com/dotnet/runtime/issues/69919 - if (!TestLibrary.Utilities.IsNativeAot) - { - RunTestInterruptInfiniteWait(); - } + RunTestInterruptInfiniteWait(); return result; }