Skip to content

SemaphoreSlim.Wait throws on single-threaded runtimes even when the semaphore is available #134972

Closed

Description

@lewing

Description

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:

if (millisecondsTimeout == 0 && m_currentCount == 0)
{
    return false;
}

RuntimeFeature.ThrowIfMultithreadingIsNotSupported();
...

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

  1. added this to the 12.0.0 milestone on Sep 30, 2026
  2. dotnet-policy-service commented on Sep 30, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
    See info in area-owners.md if you want to be subscribed.

  3. removed
    untriagedNew issue has not been triaged by the area owner
    on Sep 30, 2026
  4. dotnet-policy-service commented on Sep 30, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @JulieLeeMSFT, @VSadov
    See info in area-owners.md if you want to be subscribed.

  5. pavelsavara commented on Oct 1, 2026

    @pavelsavara
    Member

    This is by design.
    Probably SetCurrentSolutionAsync should use async wait

  6. pavelsavara commented on Oct 1, 2026

    @pavelsavara
    Member
  7. added a commit that references this issue on Oct 17, 2026
    c5ae079
  8. added a commit that references this issue on Oct 19, 2026
    52f488e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions