Skip to content

Workspaces: Allow uncontended waits without multithreading - #85869

Draft
pavelsavara wants to merge 1 commit into
dotnet:mainfrom
pavelsavara:wasm-uncontended-semaphore-waits
Draft

pavelsavara wants to merge 1 commit into
dotnet:mainfrom
pavelsavara:wasm-uncontended-semaphore-waits

Conversation

@pavelsavara

@pavelsavara pavelsavara commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Summary

  • acquire immediately available semaphores through a zero-timeout asynchronous wait
  • retain the existing synchronous wait under contention, preserving synchronous waiter priority and blocking behavior
  • apply the behavior consistently to the Roslyn and Razor DisposableWait helpers

Motivation

SemaphoreSlim.Wait is unsupported on single-threaded runtimes even when the semaphore is immediately available. Roslyn workspace operations use internal synchronous wrappers around SemaphoreSlim, which prevented uncontended operations such as AdhocWorkspace.AddSolution and AddProject from running on single-threaded Browser and WASI.

The zero-timeout WaitAsync fast path completes synchronously when the semaphore is available. If it is unavailable, the code falls back to the existing synchronous wait so multithreaded contention and synchronous waiter priority remain unchanged.

No public API is changed.

Validation

  • dotnet build src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj -p:RunAnalyzersDuringBuild=true
    • Passed with 0 warnings and 0 errors.
  • dotnet build src/Razor/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj -p:RunAnalyzersDuringBuild=true
    • Passed with 0 warnings and 0 errors.
  • dotnet test src/Workspaces/CoreTest/Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj --filter "FullyQualifiedName~AdhocWorkspaceTests|FullyQualifiedName~ProjectDependencyGraphTests"
    • net10.0: 46 passed, 0 failed.
    • net472: 46 passed, 0 failed.
  • dotnet test src/Razor/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.UnitTests/Microsoft.CodeAnalysis.Razor.Workspaces.UnitTests.csproj --filter "FullyQualifiedName~RazorCodeDocumentExtensionsTest"
    • net10.0: 21 passed, 0 failed.
    • net472: 21 passed, 0 failed.
  • dotnet test src/Razor/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests/Microsoft.VisualStudio.LanguageServices.Razor.UnitTests.csproj --filter "FullyQualifiedName~HtmlDocumentSynchronizerTest"
    • net472: 12 passed, 0 failed.
  • git diff --check
    • Passed.

Direct Browser/WASI execution was not run.

Related: dotnet/runtime#134972

Resolves #84615

Note

This pull request was prepared with assistance from GitHub Copilot.

Microsoft Reviewers: Open in CodeFlow

Try an immediate asynchronous acquisition before falling back to the
existing synchronous wait. This allows single-threaded runtimes to acquire
available semaphores while preserving synchronous waiter priority under
contention.

Related to dotnet/runtime#134972 and dotnet#84615.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 2 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service dotnet-policy-service Bot added Community The pull request was submitted by a contributor who is not a Microsoft employee. Area-Razor labels Oct 1, 2026
semaphore.Wait(cancellationToken);
// WaitAsync supports an uncontended synchronous acquisition on single-threaded runtimes. Fall back
// to Wait under contention to preserve synchronous waiter priority and blocking behavior.
if (!semaphore.WaitAsync(0, cancellationToken).GetAwaiter().GetResult())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this isn't a sustainable pattern. if you want single-threaded-runtimes to work, then having them implement Wait(ct) using this pattern is fine. We're not going to chase having to patch up any and all places in the code that have an issue like this. THis is also a major footgun as it would be trivial to fall into breaking something on those runtimes at any point in teh future.

@jkotas jkotas Oct 2, 2026 •

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.

@pavelsavara Does this pattern even solve the problem with contended semaphore on single threaded runtime? I would expect it to hang or crash (same as synchronous Wait).

@CyrusNajmabadi CyrusNajmabadi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this isn't a sustainable pattern. if you want single-threaded-runtimes to work, then having them implement Wait(ct) using this pattern is fine. We're not going to chase having to patch up any and all places in the code that have an issue like this. THis is also a major footgun as it would be trivial to fall into breaking something on those runtimes at any point in teh future.

@pavelsavara

Copy link
Copy Markdown
Member Author

if you want single-threaded-runtimes to work, then having them implement Wait(ct) using this pattern is fine.

We started throwing the PNSE in Net11, in order to signal that the .Wait() API is unsupported.
It was already marked with [UnsupportedOSPlatform("browser")] for long time.

We could easily (partially) revert that change.
dotnet/runtime#123329

The problem is that the application code, like in this PR, can assume that the API just works (anyway, ignoring the UnsupportedOSPlatform). But it only works when the semaphore is uncontended.

When the semahore has (probably async) contention, the behavior is UB.
Typically spin-wait on the browser page and aw snap of chrome few minutes later.
Another example dotnet/runtime#109839

So throwing PNSE forces the application code to re-think if they support ST (browser) or not.

cc @jkotas @lewing

@jkotas

jkotas commented Oct 2, 2026

Copy link
Copy Markdown
Member

throwing PNSE forces the application code to re-think if they support ST (browser) or not.

Finding issues with the single-threaded browser model through exploration is not a predictable or scalable experience. Instead, libraries that want to support this model should run the browser compatibility analyzer. This follows the same playbook we use for trimming and AOT compatibility: the analyzer identifies all potentially problematic locations up front, giving library maintainers a concrete list to review. They can then evaluate those issues and decide whether supporting the form factor makes sense for their library.

We could easily (partially) revert that change. dotnet/runtime#123329

I would be ok with that. Again, it is same as what we do for trimming and AOT compatibility in some places. We have APIs that are marked as trimming or AOT incompatible and the docs describe the conditions under which the APIs work. The developers are expected to review their code and disable the compatibility warning with a comment that explains why the specific use is fine.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Razor Community The pull request was submitted by a contributor who is not a Microsoft employee.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Workspace APIs unusable on Blazor WebAssembly under .NET 11: AddProject takes a blocking SemaphoreSlim.Wait

3 participants