You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
SemaphoreSlim.Wait throws on single-threaded runtimes even when the semaphore is available #134972
On runtimes without multithreading support (single-threaded browser and WASI), SemaphoreSlim.Wait throws PlatformNotSupportedException even when the semaphore is available and the call would return immediately without blocking.
SemaphoreSlim.WaitCore (src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs) calls RuntimeFeature.ThrowIfMultithreadingIsNotSupported() right after the zero-timeout fast path and before it looks at m_currentCount:
WaitUntilCountOrTimeout, the method that actually blocks, has its own check, so the earlier one also rejects the uncontended case.
Impact
Code that uses a SemaphoreSlim as a mutex and never contends on it can't run single-threaded. For example, Roslyn's AdhocWorkspace takes a semaphore through SemaphoreSlimExtensions.DisposableWait:
System.PlatformNotSupportedException : Operation is not supported on this platform.
at System.Runtime.CompilerServices.RuntimeFeature.ThrowIfMultithreadingIsNotSupported()
at System.Threading.SemaphoreSlim.WaitCore(Int64 millisecondsTimeout, CancellationToken cancellationToken)
at System.Threading.SemaphoreSlim.Wait(CancellationToken cancellationToken)
at Roslyn.Utilities.SemaphoreSlimExtensions.DisposableWait(SemaphoreSlim semaphore, CancellationToken cancellationToken)
at Microsoft.CodeAnalysis.Workspace.SetCurrentSolutionAsync[TData](...)
at Microsoft.CodeAnalysis.AdhocWorkspace.AddSolution(SolutionInfo solutionInfo)
As a result, the source generator and analyzer test classes in Microsoft.Extensions.Configuration.Binder, Microsoft.Extensions.Options and System.Text.RegularExpressions are gated on IsMultithreadingSupported in the CoreCLR WASI test lanes (#134813).
Proposed change
Throw only when the call would actually have to block: take the lock and succeed if m_currentCount > 0, and throw only on the path that waits (WaitUntilCountOrTimeout, which already checks). A timeout of 0 on an unavailable semaphore should keep returning false. Check whether the synchronous Wait paths of related primitives (ManualResetEventSlim, Task.Wait on a completed task) have the same early-throw shape.
Note
This issue was drafted with the help of GitHub Copilot.
Activity
dotnet-policy-service commentedon Sep 30, 2026
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.
dotnet-policy-service commentedon Sep 30, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @VSadov
See info in area-owners.md if you want to be subscribed.
MihuBot commentedon Sep 30, 2026
I'm a bot. Here are possible related and/or duplicate issues (I may be wrong):
pavelsavara commentedon Oct 1, 2026
This is by design.
Probably SetCurrentSolutionAsync should use async wait
pavelsavara commentedon Oct 1, 2026
More discussion in dotnet/roslyn#85869 (comment)
[browser][wasi] Allow uncontended synchronous waits and un-gate Rosly…
[release/11.0] Allow uncontended synchronous waits on single-threaded…