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
[browser] SemaphoreSlim.Wait(0) throws PlatformNotSupportedException on single-threaded WASM even when acquisition would succeed without blocking #131859
On single-threaded browser-wasm (.NET 11 preview), SemaphoreSlim.Wait(0) — a zero-timeout,
non-blocking try-acquire — throws PlatformNotSupportedException whenever the semaphore has an
available count. Only the fail-fast branch escapes.
In SemaphoreSlim.WaitCore the order of checks is:
if (millisecondsTimeout == 0 && m_currentCount == 0) return false; — fast-fail path, no guard
RuntimeFeature.ThrowIfMultithreadingIsNotSupported(); — hit unconditionally otherwise
So Wait(0) against an available semaphore — an operation that by definition cannot block —
walks into the multithreading guard and throws:
System.PlatformNotSupportedException: Arg_PlatformNotSupported
at System.Runtime.CompilerServices.RuntimeFeature.ThrowIfMultithreadingIsNotSupported()
at System.Threading.SemaphoreSlim.WaitCore(Int64 millisecondsTimeout, CancellationToken cancellationToken)
at System.Threading.SemaphoreSlim.Wait(Int32 millisecondsTimeout)
Reproduction
Blazor WebAssembly app on .NET 11 preview (observed on 11.0.100-preview.6 SDK, browser-wasm,
single-threaded):
newSemaphoreSlim(1).Wait(0);// throws PlatformNotSupportedException; returned true on net10 wasm
Impact
Grpc.Net.Client ≥ 2.x calls _connectSemaphore.Wait(0) in Grpc.Net.Client.Balancer.Subchannel.ConnectTransportAsync on every channel's first connect
(load balancing is compiled into the TFM build Blazor serves, and BalancerHttpHandler wraps every
channel). On the first connect the semaphore is always available → the guard always throws → the
exception dies inside grpc-dotnet's fire-and-forget connect task (_ = RunCall(...)) → every
gRPC-Web call from Blazor WebAssembly hangs forever with zero observable errors: no console
output, no network request, an await that never completes. The same package versions work on
desktop and on the .NET 10 WASM runtime.
Expected behavior
A zero-timeout try-acquire cannot block and should not be gated on multithreading support:
the guard should sit behind the immediate-acquire fast path (mirror of the existing
fail-fast path), i.e. Wait(0) should return true/false on browser-wasm exactly as it did
through .NET 10.
Throwing PNSE on potentially blocking .Wait() in single threaded WASM is by design.
do you agree ?
It is certainly a simple story. Easy to explain and easy to analyze statically. SemaphoreSlim.Wait is marked as unsupported on browser. If grpc-dotnet is expected to work in browser, can we enable the platform compatibility analyzer in the project to catch other similar problems?
Or do you want to soften it ? (added complexity)
If we were to do anything here, we should think about all potentially blocking synchronous APIs that take timeout.
Also, can you remind me how Thread.Sleep works in browser? It is not marked as unsupported on browser that was surprising to me.
can we enable the platform compatibility analyzer in the project to catch other similar problems?
We have [UnsupportedOSPlatformAttribute("browser")] on many public APIs, but that only triggers when the library targets browser explicitly. The Grpc.Net.Client is OS neutral.
Also, can you remind me how Thread.Sleep works in browser?
It calls emscripten's pthread_cond_timedwait on a condition variable that nothing else will ever signal. It therefore blocks the current (main) thread synchronously for the full timeout, freezing the event loop - yuck.
I think making Thread.Sleep also PNSE would be too harsh for the dotnet ecosystem.
But it would the the right thing to do from browser perspective.
The correct way for browser is await Task.Delay()
but that only triggers when the library targets browser explicitly. The Grpc.Net.Client is OS neutral.
For OS neutral libraries, you can add the following to the project to check for compatibility with browser (browser is not in the default set of platforms that the compatibility is checked for):
Once you do that, you should see: C:\repro\Class1.cs(8,5): warning CA1416: This call site is reachable on all platforms. 'SemaphoreSlim.Wait(int)' is unsupported on: 'browser'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)
Activity
dotnet-policy-service commentedon Aug 5, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @VSadov
See info in area-owners.md if you want to be subscribed.
dotnet-policy-service commentedon Aug 10, 2026
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.
pavelsavara commentedon Aug 10, 2026
This is direct consequence of #123329
Throwing PNSE on potentially blocking
.Wait()in single threaded WASM is by design.@jkotas do you agree ? Or do you want to soften it ? (added complexity)
For this specific library, I created a fix grpc/grpc-dotnet#2756
jkotas commentedon Aug 10, 2026
It is certainly a simple story. Easy to explain and easy to analyze statically.
SemaphoreSlim.Waitis marked as unsupported on browser. If grpc-dotnet is expected to work in browser, can we enable the platform compatibility analyzer in the project to catch other similar problems?If we were to do anything here, we should think about all potentially blocking synchronous APIs that take timeout.
Also, can you remind me how Thread.Sleep works in browser? It is not marked as unsupported on browser that was surprising to me.
JamesNK commentedon Aug 11, 2026
The PR fix for grpc-dotnet is fine.
buvinghausen commentedon Aug 11, 2026
Thanks James and yes I only needed one side of the fix I just figured I'd raise the notion from both sides.
pavelsavara commentedon Aug 11, 2026
We have
[UnsupportedOSPlatformAttribute("browser")]on many public APIs, but that only triggers when the library targets browser explicitly. TheGrpc.Net.Clientis OS neutral.It calls emscripten's
pthread_cond_timedwaiton a condition variable that nothing else will ever signal. It therefore blocks the current (main) thread synchronously for the full timeout, freezing the event loop - yuck.I think making
Thread.Sleepalso PNSE would be too harsh for the dotnet ecosystem.But it would the the right thing to do from browser perspective.
The correct way for browser is
await Task.Delay()pavelsavara commentedon Aug 11, 2026
I tested the library fix in browser app and it seems to work with Net11 now. I'm closing this issue.
jkotas commentedon Aug 12, 2026
For OS neutral libraries, you can add the following to the project to check for compatibility with browser (browser is not in the default set of platforms that the compatibility is checked for):
Once you do that, you should see:
C:\repro\Class1.cs(8,5): warning CA1416: This call site is reachable on all platforms. 'SemaphoreSlim.Wait(int)' is unsupported on: 'browser'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)