diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigBindingGenTestDriver.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigBindingGenTestDriver.cs
index a9f9dc3e6405e0..4f869a4ade3824 100644
--- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigBindingGenTestDriver.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfigBindingGenTestDriver.cs
@@ -18,7 +18,7 @@
namespace Microsoft.Extensions.SourceGeneration.Configuration.Binder.Tests
{
- [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles), nameof(PlatformDetection.IsMultithreadingSupported))] // Roslyn workspaces block on SemaphoreSlim
+ [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))]
public partial class ConfigurationBindingGeneratorTests : ConfigurationBinderTestsBase
{
internal sealed class ConfigBindingGenTestDriver
diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Incremental.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Incremental.cs
index e219b704eeb46e..040118111effee 100644
--- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Incremental.cs
+++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Incremental.cs
@@ -12,7 +12,7 @@ namespace Microsoft.Extensions.SourceGeneration.Configuration.Binder.Tests
{
public partial class ConfigurationBindingGeneratorTests : ConfigurationBinderTestsBase
{
- [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles), nameof(PlatformDetection.IsMultithreadingSupported))] // Roslyn workspaces block on SemaphoreSlim
+ [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))]
public sealed class IncrementalTests
{
[Fact]
diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Main.cs b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Main.cs
index f4267a40a2aca2..203e0544f86213 100644
--- a/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Main.cs
+++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGeneration.Unit.Tests/Main.cs
@@ -25,7 +25,6 @@
namespace Microsoft.Gen.OptionsValidation.Unit.Test;
-[ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsMultithreadingSupported))] // Roslyn workspaces block on SemaphoreSlim
public class EmitterTests
{
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))]
diff --git a/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/EmitterTests.cs b/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/EmitterTests.cs
index 2b06b79395469a..da4aba73863aa8 100644
--- a/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/EmitterTests.cs
+++ b/src/libraries/Microsoft.Extensions.Options/tests/SourceGenerationTests/EmitterTests.cs
@@ -15,7 +15,6 @@
namespace Microsoft.Gen.OptionsValidation.Test;
-[ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsMultithreadingSupported))] // Roslyn workspaces block on SemaphoreSlim
public class EmitterTests
{
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))]
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Condition.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Condition.cs
index 34e0834290dba5..c6021a81c7b434 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/Condition.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Condition.cs
@@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Diagnostics.Tracing;
+using System.Runtime.CompilerServices;
namespace System.Threading
{
@@ -137,6 +138,9 @@ public bool Wait(int millisecondsTimeout, object associatedObjectForMonitorWait)
if (!_lock.IsHeldByCurrentThread)
throw new SynchronizationLockException();
+ if (millisecondsTimeout != 0)
+ RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
+
using ThreadBlockingInfo.Scope threadBlockingScope = new(this, millisecondsTimeout);
Waiter waiter = GetWaiterForCurrentThread();
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs
index 0ececc08ec100b..e6e3f29900f1ec 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs
@@ -513,8 +513,6 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
ArgumentOutOfRangeException.ThrowIfLessThan(millisecondsTimeout, -1);
- RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
-
if (!IsSet)
{
if (millisecondsTimeout == 0)
@@ -523,6 +521,7 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)
return false;
}
+ RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
// We spin briefly before falling back to allocating and/or waiting on a true event.
long startTime = 0;
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Monitor.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Monitor.cs
index 3f68264c559469..fbb0301c309f58 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/Monitor.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Monitor.cs
@@ -140,7 +140,6 @@ private static Lock GetLockObject(object obj)
public static bool Wait(object obj, int millisecondsTimeout)
{
ArgumentNullException.ThrowIfNull(obj);
- RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
return GetLockObject(obj).Wait(millisecondsTimeout, obj);
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs
index a530c22b051d20..6fe4227c751792 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs
@@ -320,8 +320,6 @@ private bool WaitCore(long millisecondsTimeout, CancellationToken cancellationTo
return false;
}
- RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
-
long startTime = 0;
if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout > 0)
{
@@ -367,6 +365,8 @@ private bool WaitCore(long millisecondsTimeout, CancellationToken cancellationTo
// then block on (once we've released the lock).
if (m_asyncHead is not null)
{
+ RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
+
Debug.Assert(m_asyncTail is not null, "tail should not be null if head isn't");
asyncWaitTask = WaitAsyncCore(millisecondsTimeout, cancellationToken);
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
index 6bb445bb3fe645..73ef3e5a76eb7b 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
@@ -3157,8 +3157,6 @@ private bool InternalWaitCore(int millisecondsTimeout, CancellationToken cancell
}
else
{
- RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
-
returnValue = SpinThenBlockingWait(millisecondsTimeout, cancellationToken);
}
@@ -3209,13 +3207,24 @@ internal SetOnInvokeMres() : base(false, 0) { }
/// true if the task is completed; otherwise, false.
private bool SpinThenBlockingWait(int millisecondsTimeout, CancellationToken cancellationToken)
{
+ if (!RuntimeFeature.IsMultithreadingSupported)
+ {
+ if (IsCompleted)
+ {
+ return true;
+ }
+ if (millisecondsTimeout == 0)
+ {
+ return false;
+ }
+ RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
+ }
+
bool infiniteWait = millisecondsTimeout == Timeout.Infinite;
uint startTimeTicks = infiniteWait ? 0 : (uint)Environment.TickCount;
bool returnValue = SpinWait(millisecondsTimeout);
if (!returnValue)
{
- RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
-
// We're about to block waiting for the task to complete, which is expensive, and if
// the task being waited on depends on some other work to run, this thread could end up
// waiting for some other thread to do work. If the two threads are part of the same scheduler,
@@ -3294,10 +3303,7 @@ private bool SpinWait(int millisecondsTimeout)
{
if (IsCompleted) return true;
- if (!RuntimeFeature.IsMultithreadingSupported)
- {
- return false;
- }
+ Debug.Assert(RuntimeFeature.IsMultithreadingSupported);
if (millisecondsTimeout == 0)
{
@@ -5135,8 +5141,6 @@ private static bool WaitAllCore(ReadOnlySpan tasks, int millisecondsTimeou
// If any of the waited-upon tasks end as Faulted or Canceled, set these to true.
bool exceptionSeen = false, cancellationSeen = false;
- bool returnValue = true;
-
// Collects incomplete tasks in "waitedOnTaskList"
for (int i = tasks.Length - 1; i >= 0; i--)
{
@@ -5175,29 +5179,35 @@ private static bool WaitAllCore(ReadOnlySpan tasks, int millisecondsTimeou
if (waitedOnTaskList != null)
{
- RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
+ if (!RuntimeFeature.IsMultithreadingSupported)
+ {
+ if (millisecondsTimeout != 0)
+ RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
+
+ return false;
+ }
// Block waiting for the tasks to complete.
- returnValue = WaitAllBlockingCore(waitedOnTaskList, millisecondsTimeout, cancellationToken);
+ if (!WaitAllBlockingCore(waitedOnTaskList, millisecondsTimeout, cancellationToken))
+ {
+ return false;
+ }
- // If the wait didn't time out, ensure exceptions are propagated, and if a debugger is
+ // The wait didn't time out, so ensure exceptions are propagated, and if a debugger is
// attached and one of these tasks requires it, that we notify the debugger of a wait completion.
- if (returnValue)
+ // Add any exceptions for this task to the collection, and if it's wait
+ // notification bit is set, store it to operate on at the end.
+ foreach (Task task in waitedOnTaskList)
{
- // Add any exceptions for this task to the collection, and if it's wait
- // notification bit is set, store it to operate on at the end.
- foreach (Task task in waitedOnTaskList)
- {
- if (task.IsFaulted) exceptionSeen = true;
- else if (task.IsCanceled) cancellationSeen = true;
- if (task.IsWaitNotificationEnabled) AddToList(task, ref notificationTasks, initSize: 1);
- }
+ if (task.IsFaulted) exceptionSeen = true;
+ else if (task.IsCanceled) cancellationSeen = true;
+ if (task.IsWaitNotificationEnabled) AddToList(task, ref notificationTasks, initSize: 1);
}
}
- // Now that we're done and about to exit, if the wait completed and if we have
+ // Now that we're done and about to exit, if we have
// any tasks with a notification bit set, signal the debugger if any requires it.
- if (returnValue && notificationTasks != null)
+ if (notificationTasks != null)
{
// Loop through each task tha that had its bit set, and notify the debugger
// about the first one that requires it. The debugger will reset the bit
@@ -5210,7 +5220,7 @@ private static bool WaitAllCore(ReadOnlySpan tasks, int millisecondsTimeou
}
// If one or more threw exceptions, aggregate and throw them.
- if (returnValue && (exceptionSeen || cancellationSeen))
+ if (exceptionSeen || cancellationSeen)
{
// If the WaitAll was canceled and tasks were canceled but not faulted,
// prioritize throwing an OCE for canceling the WaitAll over throwing an
@@ -5225,7 +5235,7 @@ private static bool WaitAllCore(ReadOnlySpan tasks, int millisecondsTimeou
ThrowHelper.ThrowAggregateException(exceptions);
}
- return returnValue;
+ return true;
}
/// Adds an element to the list, initializing the list if it's null.
@@ -5249,8 +5259,7 @@ private static bool WaitAllBlockingCore(List tasks, int millisecondsTimeou
{
Debug.Assert(tasks != null, "Expected a non-null list of tasks");
Debug.Assert(tasks.Count > 0, "Expected at least one task");
-
- RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
+ Debug.Assert(RuntimeFeature.IsMultithreadingSupported);
bool waitCompleted = false;
var mres = new SetOnCountdownMres(tasks.Count);
@@ -5517,7 +5526,13 @@ private static int WaitAnyCore(Task[] tasks, int millisecondsTimeout, Cancellati
if (signaledTaskIndex == -1 && tasks.Length != 0)
{
- RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
+ if (!RuntimeFeature.IsMultithreadingSupported)
+ {
+ if (millisecondsTimeout != 0)
+ RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
+
+ return -1;
+ }
Task firstCompleted = TaskFactory.CommonCWAnyLogic(tasks, isSyncBlocking: true);
bool waitCompleted = firstCompleted.Wait(millisecondsTimeout, cancellationToken);
diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/Task/TaskRtTests_Core.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/Task/TaskRtTests_Core.cs
index bf744367cdb50d..05981b1c31e9aa 100644
--- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/Task/TaskRtTests_Core.cs
+++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/Task/TaskRtTests_Core.cs
@@ -2430,6 +2430,22 @@ public static void RunTaskWaitTest()
}
}
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMultithreadingSupported))]
+ public static void Wait_SingleThreaded()
+ {
+ Task incompleteTask = new TaskCompletionSource().Task;
+
+ Task.CompletedTask.Wait();
+ Assert.True(Task.CompletedTask.Wait(0));
+ Assert.False(incompleteTask.Wait(0));
+ Assert.False(Task.WaitAll([incompleteTask], 0));
+ Assert.Equal(-1, Task.WaitAny([incompleteTask], 0));
+
+ Assert.Throws(() => incompleteTask.Wait(1));
+ Assert.Throws(() => Task.WaitAll([incompleteTask], 1));
+ Assert.Throws(() => Task.WaitAny([incompleteTask], 1));
+ }
+
// Just runs a task and waits on it.
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsMultithreadingSupported))]
public static void RunTaskWaitTest_NegativeTests()
diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/UpgradeToGeneratedRegexAnalyzerTests.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/UpgradeToGeneratedRegexAnalyzerTests.cs
index 5b2a6782928908..4daaea02f83236 100644
--- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/UpgradeToGeneratedRegexAnalyzerTests.cs
+++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/UpgradeToGeneratedRegexAnalyzerTests.cs
@@ -15,7 +15,8 @@
namespace System.Text.RegularExpressions.Tests
{
[ActiveIssue("https://github.com/dotnet/runtime/issues/69823", TestRuntimes.Mono)]
- [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles), nameof(PlatformDetection.IsMultithreadingSupported))] // Roslyn workspaces block on SemaphoreSlim
+ [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))]
+ [SkipOnPlatform(TestPlatforms.Wasi, "Roslyn's ReferenceAssemblies.ResolveAsync needs outbound network access to download packages from nuget.org, which the WASI test sandbox does not provide.")]
public class UpgradeToGeneratedRegexAnalyzerTests
{
private const string UseRegexSourceGeneratorDiagnosticId = @"SYSLIB1045";
diff --git a/src/libraries/System.Threading/tests/ManualResetEventSlimTests.cs b/src/libraries/System.Threading/tests/ManualResetEventSlimTests.cs
index 72bb8f10d5b957..4c689a1ed74186 100644
--- a/src/libraries/System.Threading/tests/ManualResetEventSlimTests.cs
+++ b/src/libraries/System.Threading/tests/ManualResetEventSlimTests.cs
@@ -68,6 +68,21 @@ public static void RunManualResetEventSlimTest2_TimeoutWait()
}
}
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMultithreadingSupported))]
+ public static void Wait_SingleThreaded()
+ {
+ using ManualResetEventSlim setEvent = new(true);
+ setEvent.Wait();
+ Assert.True(setEvent.Wait(1));
+ Assert.True(setEvent.Wait(TimeSpan.Zero));
+
+ using ManualResetEventSlim unsetEvent = new(false);
+ Assert.False(unsetEvent.Wait(0));
+ Assert.False(unsetEvent.Wait(TimeSpan.Zero));
+ Assert.Throws(() => unsetEvent.Wait(1));
+ Assert.Throws(() => unsetEvent.Wait());
+ }
+
// Tests timeout on an event that is never set.
[Fact]
public static void RunManualResetEventSlimTest3_ConstructorTests()
diff --git a/src/libraries/System.Threading/tests/MonitorTests.cs b/src/libraries/System.Threading/tests/MonitorTests.cs
index a575afe7b916b0..fe82de6b89faf7 100644
--- a/src/libraries/System.Threading/tests/MonitorTests.cs
+++ b/src/libraries/System.Threading/tests/MonitorTests.cs
@@ -413,6 +413,19 @@ public static void Wait_Invalid()
() => Monitor.Wait(obj, TimeSpan.FromMilliseconds((double)int.MaxValue + 1)));
}
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMultithreadingSupported))]
+ public static void Wait_SingleThreaded()
+ {
+ object obj = new();
+ lock (obj)
+ {
+ Assert.False(Monitor.Wait(obj, 0));
+ Assert.False(Monitor.Wait(obj, TimeSpan.Zero));
+ Assert.Throws(() => Monitor.Wait(obj, 1));
+ Assert.Throws(() => Monitor.Wait(obj));
+ }
+ }
+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsMultithreadingSupported))]
public static void WaitTest()
{
diff --git a/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs b/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs
index 194dbbebd8df8a..b90a6d2be4e3b2 100644
--- a/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs
+++ b/src/libraries/System.Threading/tests/SemaphoreSlimTests.cs
@@ -63,6 +63,26 @@ public static void RunSemaphoreSlimTest1_Wait()
RunSemaphoreSlimTest1_Wait_Helper(1, 10, TimeSpan.FromMilliseconds(uint.MaxValue), true, null);
}
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMultithreadingSupported))]
+ public static void Wait_SingleThreaded()
+ {
+ using SemaphoreSlim semaphore = new(1, 1);
+
+ semaphore.Wait();
+ semaphore.Release();
+ semaphore.Wait(CancellationToken.None);
+ semaphore.Release();
+ Assert.True(semaphore.Wait(1));
+ semaphore.Release();
+ Assert.True(semaphore.Wait(TimeSpan.FromMilliseconds(1)));
+ semaphore.Release();
+ Assert.True(semaphore.Wait(0, CancellationToken.None));
+
+ Assert.False(semaphore.Wait(0));
+ Assert.Throws(() => semaphore.Wait(1));
+ Assert.Throws(() => semaphore.Wait());
+ }
+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsMultithreadingSupported))]
public static void RunSemaphoreSlimTest1_Wait_NegativeCases()
{
diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/Monitor.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/Monitor.Mono.cs
index dad53629506b8f..7f248522770673 100644
--- a/src/mono/System.Private.CoreLib/src/System/Threading/Monitor.Mono.cs
+++ b/src/mono/System.Private.CoreLib/src/System/Threading/Monitor.Mono.cs
@@ -77,7 +77,6 @@ public static bool IsEntered(object obj)
public static bool Wait(object obj, int millisecondsTimeout)
{
ArgumentNullException.ThrowIfNull(obj);
- RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
return ObjWait(millisecondsTimeout, obj);
}
@@ -126,6 +125,9 @@ private static bool ObjWait(int millisecondsTimeout, object obj)
if (!ObjectHeader.HasOwner(obj))
throw new SynchronizationLockException();
+ if (millisecondsTimeout != 0)
+ RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
+
bool sendWaitEvents =
millisecondsTimeout != 0 &&
NativeRuntimeEventSource.Log.IsEnabled(EventLevel.Verbose, NativeRuntimeEventSource.Keywords.WaitHandleKeyword);
diff --git a/src/mono/wasm/threads.md b/src/mono/wasm/threads.md
index 2931e6172738c2..781482496237bc 100644
--- a/src/mono/wasm/threads.md
+++ b/src/mono/wasm/threads.md
@@ -39,6 +39,12 @@ JIT/interpreter/AOT to drop the multi-threaded implementation in the single-thre
The implementation should not use `[UnsupportedOSPlatform("browser")]`
+Synchronous wait APIs that are unsupported without multithreading should call
+`ThrowIfMultithreadingIsNotSupported()` only when they would actually block. They can succeed when
+a semaphore count, task completion, or set state is already available, and a zero-timeout wait
+should return its normal result. The public APIs remain annotated as unsupported because whether a
+wait blocks depends on runtime state.
+
## Native runtime preprocessor defines ##
In `src/mono/mono` and `src/mono/wasm` `DISABLE_THREADS` is defined for single-threaded builds (same