Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Runtime.CompilerServices;

namespace System.Threading
{
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,8 +513,6 @@ public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken)

ArgumentOutOfRangeException.ThrowIfLessThan(millisecondsTimeout, -1);

RuntimeFeature.ThrowIfMultithreadingIsNotSupported();

if (!IsSet)
{
if (millisecondsTimeout == 0)
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,6 @@ private bool WaitCore(long millisecondsTimeout, CancellationToken cancellationTo
return false;
}

RuntimeFeature.ThrowIfMultithreadingIsNotSupported();

long startTime = 0;
if (millisecondsTimeout != Timeout.Infinite && millisecondsTimeout > 0)
{
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3157,8 +3157,6 @@ private bool InternalWaitCore(int millisecondsTimeout, CancellationToken cancell
}
else
{
RuntimeFeature.ThrowIfMultithreadingIsNotSupported();

returnValue = SpinThenBlockingWait(millisecondsTimeout, cancellationToken);
}

Expand Down Expand Up @@ -3212,7 +3210,7 @@ private bool SpinThenBlockingWait(int millisecondsTimeout, CancellationToken can
bool infiniteWait = millisecondsTimeout == Timeout.Infinite;
uint startTimeTicks = infiniteWait ? 0 : (uint)Environment.TickCount;
bool returnValue = SpinWait(millisecondsTimeout);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!RuntimeFeature.IsMultithreadingSupported) in SpinWait can be deleted or replaced by assert.

if (!returnValue)
if (!returnValue && (millisecondsTimeout != 0 || RuntimeFeature.IsMultithreadingSupported))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!returnValue && (millisecondsTimeout != 0 || RuntimeFeature.IsMultithreadingSupported))
if (!returnValue)

{
RuntimeFeature.ThrowIfMultithreadingIsNotSupported();

Comment on lines 3215 to 3216

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
RuntimeFeature.ThrowIfMultithreadingIsNotSupported();

Expand Down Expand Up @@ -5175,10 +5173,17 @@ private static bool WaitAllCore(ReadOnlySpan<Task> tasks, int millisecondsTimeou

if (waitedOnTaskList != null)
{
RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
if (millisecondsTimeout != 0 || RuntimeFeature.IsMultithreadingSupported)
{
RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
Comment on lines +5176 to +5178

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (millisecondsTimeout != 0 || RuntimeFeature.IsMultithreadingSupported)
{
RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
if (!RuntimeFeature.IsMultithreadingSupported)
{
if (millisecondsTimeout != 0)
RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
return false;
}
// Block waiting for the tasks to complete.
if (!WaitAllBlockingCore(waitedOnTaskList, millisecondsTimeout, cancellationToken))
{
return false;
}
  • delete returnValue from the method since it is always going to be true now

  • RuntimeFeature.ThrowIfMultithreadingIsNotSupported(); in WaitAllBlockingCore can be deleted or replaced by assert


// Block waiting for the tasks to complete.
returnValue = WaitAllBlockingCore(waitedOnTaskList, millisecondsTimeout, cancellationToken);
// Block waiting for the tasks to complete.
returnValue = WaitAllBlockingCore(waitedOnTaskList, millisecondsTimeout, cancellationToken);
}
else
{
returnValue = false;
}

// If the wait didn't time out, 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.
Expand Down Expand Up @@ -5515,7 +5520,9 @@ private static int WaitAnyCore(Task[] tasks, int millisecondsTimeout, Cancellati
}
}

if (signaledTaskIndex == -1 && tasks.Length != 0)
if (signaledTaskIndex == -1 &&
tasks.Length != 0 &&
(millisecondsTimeout != 0 || RuntimeFeature.IsMultithreadingSupported))
Comment on lines +5523 to +5525

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (signaledTaskIndex == -1 &&
tasks.Length != 0 &&
(millisecondsTimeout != 0 || RuntimeFeature.IsMultithreadingSupported))
if (signaledTaskIndex == -1 && tasks.Length != 0)

{
RuntimeFeature.ThrowIfMultithreadingIsNotSupported();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
if (!RuntimeFeature.IsMultithreadingSupported)
{
if (millisecondsTimeout != 0)
RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
return -1;
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<PlatformNotSupportedException>(() => incompleteTask.Wait(1));
Assert.Throws<PlatformNotSupportedException>(() => Task.WaitAll([incompleteTask], 1));
Assert.Throws<PlatformNotSupportedException>(() => Task.WaitAny([incompleteTask], 1));
}

// Just runs a task and waits on it.
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsMultithreadingSupported))]
public static void RunTaskWaitTest_NegativeTests()
Expand Down
15 changes: 15 additions & 0 deletions src/libraries/System.Threading/tests/ManualResetEventSlimTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PlatformNotSupportedException>(() => unsetEvent.Wait(1));
Assert.Throws<PlatformNotSupportedException>(() => unsetEvent.Wait());
}

// Tests timeout on an event that is never set.
[Fact]
public static void RunManualResetEventSlimTest3_ConstructorTests()
Expand Down
13 changes: 13 additions & 0 deletions src/libraries/System.Threading/tests/MonitorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PlatformNotSupportedException>(() => Monitor.Wait(obj, 1));
Assert.Throws<PlatformNotSupportedException>(() => Monitor.Wait(obj));
}
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsMultithreadingSupported))]
public static void WaitTest()
{
Expand Down
20 changes: 20 additions & 0 deletions src/libraries/System.Threading/tests/SemaphoreSlimTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PlatformNotSupportedException>(() => semaphore.Wait(1));
Assert.Throws<PlatformNotSupportedException>(() => semaphore.Wait());
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsMultithreadingSupported))]
public static void RunSemaphoreSlimTest1_Wait_NegativeCases()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions src/mono/wasm/threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading