[browser] Simplify synchronization primitives#125084
[browser] Simplify synchronization primitives#125084pavelsavara wants to merge 3 commits intodotnet:mainfrom
Conversation
|
Tagging subscribers to this area: @agocke, @VSadov |
There was a problem hiding this comment.
Pull request overview
This PR simplifies low-level synchronization primitives for single-threaded WASM targets (FEATURE_SINGLE_THREADED) by removing atomic/spin/wait behavior that can’t be meaningfully exercised without multithreading, and by compiling out the Unix WaitSubsystem to reduce overhead.
Changes:
- Replaces
Interlocked-based locking/unlocking paths with plain reads/writes (and removes spinning) underFEATURE_SINGLE_THREADED. - Compiles out the Unix WaitSubsystem and Condition implementation on single-threaded targets, adding minimal stubs where required for type/layout.
- Stubs various Unix wait-handle operations (WaitOne/SignalAndWait, Event/Mutex/Semaphore operations, Thread wait-related hooks) with
PlatformNotSupportedExceptionon single-threaded targets.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs | Gates Mono thread wait-info usage on !FEATURE_SINGLE_THREADED; throws for WaitInfo on ST. |
| src/mono/System.Private.CoreLib/src/System/Threading/ObjectHeader.Mono.cs | Removes CAS-based lock-word operations on ST; uses plain header/status writes. |
| src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.WaitableObject.Unix.cs | Compiles out WaitSubsystem waitable objects on ST. |
| src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.Unix.cs | Compiles out WaitSubsystem core implementation on ST. |
| src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.ThreadWaitInfo.Unix.cs | Compiles out ThreadWaitInfo implementation on ST and adds minimal stub type. |
| src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.HandleManager.Unix.cs | Compiles out handle manager on ST. |
| src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Unix.cs | Stubs wait operations to throw PlatformNotSupportedException on ST. |
| src/libraries/System.Private.CoreLib/src/System/Threading/ThreadBlockingInfo.cs | Gates Condition-related blocking info on !FEATURE_SINGLE_THREADED. |
| src/libraries/System.Private.CoreLib/src/System/Threading/Thread.Unix.cs | Stubs sleep/join internals on ST. |
| src/libraries/System.Private.CoreLib/src/System/Threading/SpinLock.cs | Removes spinning/Interlocked operations on ST; implements try-once semantics. |
| src/libraries/System.Private.CoreLib/src/System/Threading/Semaphore.Unix.cs | Stubs semaphore creation/open/release on ST. |
| src/libraries/System.Private.CoreLib/src/System/Threading/Mutex.Unix.cs | Stubs mutex creation/open/release on ST; refactors name handling placement. |
| src/libraries/System.Private.CoreLib/src/System/Threading/Monitor.cs | Compiles out object→Condition mapping on ST; stubs Wait/Pulse/PulseAll. |
| src/libraries/System.Private.CoreLib/src/System/Threading/LowLevelLock.cs | Simplifies LowLevelLock to plain state ops on ST and compiles out wait paths. |
| src/libraries/System.Private.CoreLib/src/System/Threading/Lock.cs | Major ST-only simplification: removes state machine/spin/wait logic; plain field ops and stubs. |
| src/libraries/System.Private.CoreLib/src/System/Threading/EventWaitHandle.Unix.cs | Stubs event creation/open/set/reset on ST. |
| src/libraries/System.Private.CoreLib/src/System/Threading/Condition.cs | Compiles out Condition implementation on ST. |
| src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/NativeRuntimeEventSource.Threading.cs | Gates lock-contention event overloads that depend on MT-only Lock APIs. |
| src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/NativeRuntimeEventSource.Threading.NativeSinks.cs | Same gating for native sink overloads. |
| src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeWaitHandle.Unix.cs | Avoids WaitSubsystem handle deletion on ST. |
| src/coreclr/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs | Gates CoreCLR wait-subsystem interactions on !FEATURE_SINGLE_THREADED; throws for WaitInfo on ST. |
| src/coreclr/System.Private.CoreLib/src/System/Threading/ObjectHeader.CoreCLR.cs | Removes InternalCall acquire/release on ST; directly manipulates header bits without CAS/spin. |
| private static void SleepInternal(int millisecondsTimeout) => | ||
| throw new PlatformNotSupportedException(); |
There was a problem hiding this comment.
On FEATURE_SINGLE_THREADED, Thread.Sleep will always throw PlatformNotSupportedException. This is likely to surface in common code paths like SpinWait.SpinOnce() (which calls Thread.Sleep(0) / Thread.Sleep(1) after enough yields), causing unexpected exceptions even when callers only intend to yield. Consider at least supporting the non-blocking millisecondsTimeout == 0 case by mapping it to UninterruptibleSleep0()/Thread.Yield(), and only throwing for blocking timeouts (> 0 or Infinite), or alternatively adjusting the call sites to avoid Thread.Sleep on single-threaded targets.
| private static void SleepInternal(int millisecondsTimeout) => | |
| throw new PlatformNotSupportedException(); | |
| private static void SleepInternal(int millisecondsTimeout) | |
| { | |
| if (millisecondsTimeout == 0) | |
| { | |
| UninterruptibleSleep0(); | |
| return; | |
| } | |
| throw new PlatformNotSupportedException(); | |
| } |
| out bool createdNew) => | ||
| throw new PlatformNotSupportedException(); | ||
|
|
||
| private static OpenExistingResult OpenExistingWorker( | ||
| string name, | ||
| NamedWaitHandleOptionsInternal options, | ||
| out Semaphore? result) => | ||
| throw new PlatformNotSupportedException(); |
There was a problem hiding this comment.
In the FEATURE_SINGLE_THREADED stub for the named overload of CreateSemaphoreCore, ValidateArguments(initialCount, maximumCount) is no longer called (it is called in the non-stub implementation). This changes exception behavior for invalid counts (ArgumentOutOfRange/ArgumentException) to PlatformNotSupportedException. Consider keeping the same argument validation (and potentially the same SR message for named primitives) before throwing so callers get consistent input-validation exceptions.
| out bool createdNew) => | |
| throw new PlatformNotSupportedException(); | |
| private static OpenExistingResult OpenExistingWorker( | |
| string name, | |
| NamedWaitHandleOptionsInternal options, | |
| out Semaphore? result) => | |
| throw new PlatformNotSupportedException(); | |
| out bool createdNew) | |
| { | |
| ValidateArguments(initialCount, maximumCount); | |
| createdNew = false; | |
| if (name is not null) | |
| { | |
| throw new PlatformNotSupportedException(SR.PlatformNotSupported_NamedSynchronizationPrimitives); | |
| } | |
| throw new PlatformNotSupportedException(); | |
| } | |
| private static OpenExistingResult OpenExistingWorker( | |
| string name, | |
| NamedWaitHandleOptionsInternal options, | |
| out Semaphore? result) => | |
| throw new PlatformNotSupportedException(SR.PlatformNotSupported_NamedSynchronizationPrimitives); |
| out bool createdNew) => | ||
| throw new PlatformNotSupportedException(); |
There was a problem hiding this comment.
In the FEATURE_SINGLE_THREADED stub for the named overload of CreateEventCore, ValidateMode(mode) is no longer called (it is called in the non-stub implementation). This changes exception behavior for invalid EventResetMode values to PlatformNotSupportedException. Consider validating mode (and any other argument checks that normally run) before throwing so the API preserves its argument-validation behavior even when the operation itself isn’t supported on single-threaded targets.
| out bool createdNew) => | |
| throw new PlatformNotSupportedException(); | |
| out bool createdNew) | |
| { | |
| ValidateMode(mode); | |
| if (name is not null) | |
| { | |
| throw new PlatformNotSupportedException(SR.PlatformNotSupported_NamedSynchronizationPrimitives); | |
| } | |
| throw new PlatformNotSupportedException(); | |
| } |
| out Mutex? result) => | ||
| throw new PlatformNotSupportedException(); |
There was a problem hiding this comment.
The non-FEATURE_SINGLE_THREADED implementation of OpenExistingWorker throws ArgumentException for null/empty name before attempting to open the mutex. The FEATURE_SINGLE_THREADED stub now throws PlatformNotSupportedException unconditionally, which changes input-validation behavior for Mutex.OpenExisting/TryOpenExisting (e.g., empty name). Consider keeping the same ArgumentException.ThrowIfNullOrEmpty(name) validation before throwing so callers get consistent parameter-validation exceptions.
| out Mutex? result) => | |
| throw new PlatformNotSupportedException(); | |
| out Mutex? result) | |
| { | |
| ArgumentException.ThrowIfNullOrEmpty(name); | |
| throw new PlatformNotSupportedException(); | |
| } |
Simplify synchronization primitives for single-threaded browser targets
Summary
Replace
Interlockedoperations and spinning/waiting logic with plain reads and writes across all synchronization primitives on single-threaded browser WASM targets (FEATURE_SINGLE_THREADED). On a single-threaded runtime, atomic operations and spin loops are both unnecessary and expensive (especially under the interpreter), and blocking waits are impossible since no other thread can release a lock.Motivation
On the single-threaded browser target:
Interlocked.CompareExchange/Decrement/Exchangeare unnecessary — there's only one thread, so plain reads/writes are sufficient and cheaper under the interpreter.These changes eliminate this overhead at compile time using
#if FEATURE_SINGLE_THREADED, ensuring both debug and release builds benefit.Changes
22 files changed, ~620 insertions, ~24 deletions
Lock.cs — Fast path and slow path simplification
TryEnter_Inlined: Plain_owningThreadIdread/write instead ofState.TryLock()(which usesInterlocked.CompareExchange)ExitImpl: Plain_owningThreadId/_recursionCountwrites instead ofState.Unlock()(which usesInterlocked.Decrement)TryEnterSlow: Handles recursion via plain field access; throwsPlatformNotSupportedExceptionfor unacquirable locks (deadlock on MT)ExitAll/Reenter/TryEnterOneShot: Plain field operationsStatestruct (~550 lines),TryLockResultenum,StaticsInitializationStageenumLazyInitializeOrEnter,TryInitializeStatics, spin/waiter helpers,CreateWaitEvent,SignalWaiterIfNecessary_spinCount,_waiterStartTimeMs,_waitEvent, all static fieldsContentionCountreturns 0;Dispose()is no-op;IsSingleProcessorreturnstrueObjectHeader.CoreCLR.cs — Thin lock without CAS
TryAcquireThinLock: Reads/writes header directly withoutInterlocked.CompareExchange— checks hash/syncblock → UseSlowPath, checks same thread → increment recursion via plain write, checks free → write threadIDRelease: Plain header writes instead of CASTryAcquireThinLockSpin: Entire spin loop gated outAcquireInternal(InternalCall): Gated out on STObjectHeader.Mono.cs — Lock word without CAS
TryEnterInflatedFast: Plain write toSyncBlock.Statusinstead ofInterlocked.CompareExchangeTryEnterFast: Plain write (h.Header.synchronization) instead ofLockWordCompareExchangeTryExitInflated/TryExitFlat: Plain writes, always succeeds on STSpinLock.cs — Eliminate spinning entirely
CompareExchangehelper: Plain read/write instead ofInterlocked.CompareExchangeContinueTryEnter: Try lock once (no spinning/yielding/waiter management)ContinueTryEnterWithThreadTracking: Try once (no spin loop)Exit/ExitSlowPath: Plain writes instead ofInterlocked.Decrement/ExchangeDecrementWaiters: Gated out entirely (no waiters on ST)SLEEP_ONE_FREQUENCY,TIMEOUT_CHECK_FREQUENCY,WAITERS_MASK,MAXIMUM_WAITERSLowLevelLock.cs — Plain lock/unlock
TryAcquire: Plain_stateread/write instead ofInterlocked.CompareExchangeRelease:_state = 0instead ofInterlocked.Decrement+SignalWaiterAcquire:Debug.Fail+ throw ifTryAcquirefails (should never happen on ST)WaitAndAcquire,SignalWaiter,TryAcquire_NoFastPath, MT-only fields/constantsMonitor.cs — Wait/Pulse stubs
Wait: CallsThrowIfMultithreadingIsNotSupported()thenreturn defaultPulse/PulseAll: No-ops (just null check)s_conditionTable,GetCondition: Gated out; stubs_conditionTable = nullkept for CoreCLR VMCondition.cs — Entire file gated
#if !FEATURE_SINGLE_THREADED— Wait/Pulse impossible on STWaitSubsystem (4 files) — Entire subsystem gated
WaitSubsystem.Unix.cs,WaitSubsystem.WaitableObject.Unix.cs,WaitSubsystem.HandleManager.Unix.cs: Wrapped with#if !FEATURE_SINGLE_THREADEDWaitSubsystem.ThreadWaitInfo.Unix.cs: Gated +#elsestub with emptyThreadWaitInfoclass (required by CoreCLR VMThreadBaseObject._waitInfofield layout)Unix wait primitives — Throwing stubs
WaitHandle.Unix.cs:WaitOneCore,WaitMultipleIgnoringSyncContextCore,SignalAndWaitCore→ throwPlatformNotSupportedExceptionEventWaitHandle.Unix.cs:CreateEventCore,Reset,Set→ throwMutex.Unix.cs:CreateMutexCore,ReleaseMutex→ throwSemaphore.Unix.cs:CreateSemaphoreCore,ReleaseCore→ throwSafeWaitHandle.Unix.cs:WaitSubsystem.DeleteHandlecall gatedThread.Unix.cs:SleepInternal→ throw;JoinInternal→ throw;SetJoinHandle→ emptyThread files — WaitSubsystem entry point disconnection
Thread.CoreCLR.cs:WaitInfoproperty → throw on ST;Interrupt→ skip WaitSubsystem call;OnThreadExiting→ skip WaitInfo cleanupThread.Mono.cs: Same pattern;_waitInfofield gated out (safe on Mono — no VM layout constraint)Supporting changes
ThreadBlockingInfo.cs:Conditionreferences gated with&& !FEATURE_SINGLE_THREADEDNativeRuntimeEventSource.Threading.cs+.NativeSinks.cs: Lock contention event overloads gated (referenceLockIdForEventswhich doesn't exist on ST)Testing
Design decisions
#if FEATURE_SINGLE_THREADED(compile-time) over runtime checks: Ensures dead code is eliminated in both debug and release builds, and avoids interpreter overhead for checkingRuntimeFeature.IsMultithreadingSupportedon every lock operation.Keep
_statefield in Lock.cs: Required by CoreCLR VM viaDEFINE_FIELD(LOCK, STATE, _state)incorelib.h. The field exists but is unused on ST.Keep
WaitSubsystem.ThreadWaitInfoas stub: CoreCLR'sThreadBaseObject(inobject.h) defines_waitInfoat a fixed layout offset. The empty stub class satisfies the type reference without pulling in the WaitSubsystem dependency chain.LowLevelLock kept functional (not gated entirely): Has consumers beyond WaitSubsystem —
ThreadPoolWorkQueue,ThreadInt64PersistentCounter,PortableThreadPool. Simplified to plain read/write instead of removed.SpinLock try-once semantics: On ST,
ContinueTryEntertries to acquire the lock exactly once. If it fails, it returnslockTaken=false(for timeout=0) or was already tried in the fast path. No spinning since it would deadlock.