Repository navigation
[browser][wasi] Allow uncontended synchronous waits - #135335
pavelsavara wants to merge 3 commits into
Conversation
Single-threaded runtimes cannot make progress once a synchronous wait blocks, but immediately satisfiable and zero-timeout waits do not require multithreading. Move the platform guard to actual blocking paths for synchronization objects and tasks, and cover the behavior on Browser. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 4 pipeline(s). 12 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @JulieLeeMSFT, @VSadov |
|
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara |
Keep WaitHandle timeout behavior unchanged. These APIs are supported on Browser and dotnet#123329 explicitly removed their blocking guard. Limit the conditional PNSE behavior to the synchronous APIs that already throw on runtimes without multithreading. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@lewing please help me to get this finished and merged into Net11, so that we avoid behavior breaking change. Thanks |
Limit the zero-timeout Task shortcuts to runtimes without multithreading. Multithreaded targets continue using the original blocking setup and continuation paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| /// <param name="cancellationToken">The token.</param> | ||
| /// <returns>true if the task is completed; otherwise, false.</returns> | ||
| private bool SpinThenBlockingWait(int millisecondsTimeout, CancellationToken cancellationToken) | ||
| { |
There was a problem hiding this comment.
| { | |
| if (!RuntimeFeature.IsMultithreadingSupported) | |
| { | |
| if (IsCompleted) | |
| { | |
| return true; | |
| } | |
| if (millisecondsTimeout == 0) | |
| { | |
| return false; | |
| } | |
| RuntimeFeature.ThrowIfMultithreadingIsNotSupported(); | |
| } |
I think it would be easier to understand if the ST handling is in a separate block like this. It also a bit smaller since we avoid calling Environment.TickCount.
| uint startTimeTicks = infiniteWait ? 0 : (uint)Environment.TickCount; | ||
| bool returnValue = SpinWait(millisecondsTimeout); | ||
| if (!returnValue) | ||
| if (!returnValue && (millisecondsTimeout != 0 || RuntimeFeature.IsMultithreadingSupported)) |
There was a problem hiding this comment.
| if (!returnValue && (millisecondsTimeout != 0 || RuntimeFeature.IsMultithreadingSupported)) | |
| if (!returnValue) |
| RuntimeFeature.ThrowIfMultithreadingIsNotSupported(); | ||
|
|
There was a problem hiding this comment.
| RuntimeFeature.ThrowIfMultithreadingIsNotSupported(); |
| { | ||
| bool infiniteWait = millisecondsTimeout == Timeout.Infinite; | ||
| uint startTimeTicks = infiniteWait ? 0 : (uint)Environment.TickCount; | ||
| bool returnValue = SpinWait(millisecondsTimeout); |
There was a problem hiding this comment.
if (!RuntimeFeature.IsMultithreadingSupported) in SpinWait can be deleted or replaced by assert.
| if (signaledTaskIndex == -1 && | ||
| tasks.Length != 0 && | ||
| (millisecondsTimeout != 0 || RuntimeFeature.IsMultithreadingSupported)) |
There was a problem hiding this comment.
| if (signaledTaskIndex == -1 && | |
| tasks.Length != 0 && | |
| (millisecondsTimeout != 0 || RuntimeFeature.IsMultithreadingSupported)) | |
| if (signaledTaskIndex == -1 && tasks.Length != 0) |
| tasks.Length != 0 && | ||
| (millisecondsTimeout != 0 || RuntimeFeature.IsMultithreadingSupported)) | ||
| { | ||
| RuntimeFeature.ThrowIfMultithreadingIsNotSupported(); |
There was a problem hiding this comment.
| RuntimeFeature.ThrowIfMultithreadingIsNotSupported(); | |
| if (!RuntimeFeature.IsMultithreadingSupported) | |
| { | |
| if (millisecondsTimeout != 0) | |
| RuntimeFeature.ThrowIfMultithreadingIsNotSupported(); | |
| return -1; | |
| } |
| if (millisecondsTimeout != 0 || RuntimeFeature.IsMultithreadingSupported) | ||
| { | ||
| RuntimeFeature.ThrowIfMultithreadingIsNotSupported(); |
There was a problem hiding this comment.
| 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
Summary
PlatformNotSupportedExceptiononly when a synchronous wait would actually block on a runtime without multithreadingSemaphoreSlim,ManualResetEventSlim, tasks, and monitorsThread.SleepandWaitHandlebehavior unchangedThe APIs remain annotated as unsupported on Browser. The compatibility warning is still necessary because whether a synchronous wait blocks depends on runtime state.
Behavior comparison
The following table compares single-threaded Browser behavior:
SemaphoreSlimcountManualResetEventSlimTask.Wait()SemaphoreSlim.Wait(0)falsefalsefalseManualResetEventSlim.Wait(0)falsefalseTask.Wait(0)/WaitAll(..., 0)/WaitAny(..., 0)Monitor.Wait(obj, 0)falseafter releasing and reacquiring the monitorfalseafter releasing and reacquiring the monitorSemaphoreSlimorManualResetEventSlimwith positive/infinite timeoutWaitHandle.WaitOne,WaitAny,WaitAll, andSignalAndWaitWASI already threw PNSE unconditionally for several synchronous waits before #123329. This change allows its immediately satisfiable and zero-timeout cases while retaining PNSE for actual blocking.
Motivation
The fail-fast behavior from #123329 prevents browser event-loop deadlocks, but it also rejects waits that can be proven not to block. This prevents otherwise single-thread-compatible libraries from using synchronous wrappers around uncontended synchronization objects.
The updated behavior follows the compatibility-analyzer model discussed in dotnet/roslyn#85869: the APIs remain marked unsupported, while reviewed call sites can work when their state guarantees immediate completion.
Testing
.\build.cmd -bl -os browser -subset clr+libs+host -c Debug.\build.cmd -bl -os browser -subset clr.corelib+clr.nativecorelib+libs.pretest -c Debug /p:RuntimeFlavor=CoreCLR.\dotnet.cmd build -bl /p:TargetOS=browser /p:TargetArchitecture=wasm /p:Configuration=Debug /p:RuntimeFlavor=CoreCLR /t:Test /p:Scenario=WasmTestOnV8 .\src\libraries\System.Threading\tests\System.Threading.Tests.csprojSystem.Threading.Tasks.Tests.TaskRtTests_Core:Microsoft.Extensions.Caching.Memory.TokenExpirationTests.TokenExpiresOnRegister: 1 passed.System.Threading.Tests.TimerFiringTests.Timer_FiresOnlyOnce_OnDueTime_With_InfinitePeriod: 1 passed.System.Diagnostics.Tests.StopwatchTests: 6 passed.git diff --checkDirect Mono and WASI execution was not run.
Resolves #134972
Note
This pull request was prepared with assistance from GitHub Copilot.