diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj
index 2c96332adf7d00..7363ef34e2730d 100644
--- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj
+++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj
@@ -284,6 +284,12 @@
Interop\Windows\Kernel32\Interop.DynamicLoad.cs
+
+ Interop\Windows\Kernel32\Interop.QueueUserAPC.cs
+
+
+ Interop\Windows\Kernel32\Interop.Threading.cs
+
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 12a2273eddf605..d8089758b317a7 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
@@ -20,10 +20,15 @@ public sealed partial class Thread
[ThreadStatic]
private static ComState t_comState;
+ [ThreadStatic]
+ private static bool t_interruptRequested;
+
private SafeWaitHandle _osHandle;
private ApartmentState _initialApartmentState = ApartmentState.Unknown;
+ private volatile bool _pendingInterrupt;
+
partial void PlatformSpecificInitialize();
// Platform-specific initialization of foreign threads, i.e. threads not created by Thread.Start
@@ -162,7 +167,40 @@ private bool JoinInternal(int millisecondsTimeout)
}
else
{
- result = WaitHandle.WaitOneCore(waitHandle.DangerousGetHandle(), millisecondsTimeout, useTrivialWaits: false);
+ Thread? currentThread = t_currentThread;
+
+ // Check for pending interrupt from before thread started
+ if (currentThread is not null && currentThread._pendingInterrupt)
+ {
+ currentThread._pendingInterrupt = false;
+ throw new ThreadInterruptedException();
+ }
+
+ if (currentThread is not null)
+ {
+ currentThread.SetWaitSleepJoinState();
+ }
+
+ try
+ {
+ // Use alertable wait so we can be interrupted by APC
+ result = (int)Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(),
+ (uint)millisecondsTimeout, Interop.BOOL.TRUE);
+
+ // Check if we were interrupted by an APC
+ if (result == Interop.Kernel32.WAIT_IO_COMPLETION)
+ {
+ CheckForInterrupt();
+ return false; // Interrupted, so join did not complete
+ }
+ }
+ finally
+ {
+ if (currentThread is not null)
+ {
+ currentThread.ClearWaitSleepJoinState();
+ }
+ }
}
return result == (int)Interop.Kernel32.WAIT_OBJECT_0;
@@ -226,6 +264,21 @@ private static uint ThreadEntryPoint(IntPtr parameter)
return 0;
}
+ private static void CheckPendingInterrupt()
+ {
+ Thread? currentThread = t_currentThread;
+ if (currentThread is not null && currentThread._pendingInterrupt)
+ {
+ currentThread._pendingInterrupt = false;
+ throw new ThreadInterruptedException();
+ }
+ }
+
+ private static void CheckForPendingInterrupt()
+ {
+ CheckPendingInterrupt();
+ }
+
public ApartmentState GetApartmentState()
{
if (this != CurrentThread)
@@ -386,7 +439,51 @@ internal static Thread EnsureThreadPoolThreadInitialized()
return InitializeExistingThreadPoolThread();
}
- public void Interrupt() { throw new PlatformNotSupportedException(); }
+ [UnmanagedCallersOnly]
+ private static void InterruptApcCallback(nint parameter)
+ {
+ // This is the native APC callback that sets the interrupt flag
+ // It runs in native code to avoid managed reentrancy issues
+ t_interruptRequested = true;
+ }
+
+ private static void CheckForInterrupt()
+ {
+ if (t_interruptRequested)
+ {
+ t_interruptRequested = false;
+ throw new ThreadInterruptedException();
+ }
+ }
+
+ public void Interrupt()
+ {
+ using (_lock.EnterScope())
+ {
+ // If thread is dead, do nothing
+ if (GetThreadStateBit(ThreadState.Stopped))
+ return;
+
+ // If thread hasn't started yet, set pending interrupt flag
+ if (GetThreadStateBit(ThreadState.Unstarted))
+ {
+ _pendingInterrupt = true;
+ return;
+ }
+
+ // Queue APC to interrupt the thread
+ SafeWaitHandle osHandle = _osHandle;
+ if (osHandle is not null && !osHandle.IsInvalid && !osHandle.IsClosed)
+ {
+ nint callbackPtr;
+ unsafe
+ {
+ callbackPtr = (nint)(delegate* unmanaged)&InterruptApcCallback;
+ }
+ Interop.Kernel32.QueueUserAPC(callbackPtr, osHandle.DangerousGetHandle(), IntPtr.Zero);
+ }
+ }
+ }
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..a101c27fb23b7d 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
@@ -46,6 +46,9 @@ public sealed partial class Thread
private static int s_foregroundRunningCount;
+ // Platform-specific method to check for pending interrupts when thread starts
+ partial void CheckForPendingInterrupt();
+
private Thread()
{
_managedThreadId = System.Threading.ManagedThreadId.GetCurrentThreadId();
@@ -450,6 +453,9 @@ private static void StartThread(IntPtr parameter)
IncrementRunningForeground();
}
+ // Check for any pending interrupt that was queued before the thread started
+ thread.CheckForPendingInterrupt();
+
try
{
StartHelper? startHelper = thread._startHelper;
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueueUserAPC.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueueUserAPC.cs
new file mode 100644
index 00000000000000..fa36bd32b4922b
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueueUserAPC.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Runtime.InteropServices;
+
+internal static partial class Interop
+{
+ internal static partial class Kernel32
+ {
+ internal delegate void PAPCFUNC(nint dwParam);
+
+ [LibraryImport(Libraries.Kernel32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool QueueUserAPC(nint pfnAPC, nint hThread, nint dwData);
+ }
+}
\ No newline at end of file
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SleepEx.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SleepEx.cs
new file mode 100644
index 00000000000000..bab2a3bff71256
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SleepEx.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Runtime.InteropServices;
+
+internal static partial class Interop
+{
+ internal static partial class Kernel32
+ {
+ [LibraryImport(Libraries.Kernel32)]
+ internal static partial uint SleepEx(uint dwMilliseconds, BOOL bAlertable);
+ }
+}
\ No newline at end of file
diff --git a/src/libraries/System.Threading.Thread/tests/ThreadTests.cs b/src/libraries/System.Threading.Thread/tests/ThreadTests.cs
index 908d690e7e207d..eaa31ed026a407 100644
--- a/src/libraries/System.Threading.Thread/tests/ThreadTests.cs
+++ b/src/libraries/System.Threading.Thread/tests/ThreadTests.cs
@@ -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/tests/baseservices/threading/regressions/115178/115178.cs b/src/tests/baseservices/threading/regressions/115178/115178.cs
index 34aba004040a9d..e44d860a6b23de 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;
}