[browser][mt] Fatal `SynchronizationLockException` from `System.Threading.Lock.Exit` in `TimerQueueTimer.Fire` on multithreaded WebAssembly
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Description
In a multithreaded Blazor WebAssembly app (`true`), using
`System.Threading.Timer` leads to an **intermittent, fatal** `System.Threading.SynchronizationLockException`
thrown from inside the runtime's own timer queue — `System.Threading.Lock.Exit` called from
`TimerQueueTimer.Fire` on a thread‑pool worker. The exception is unhandled and aborts the runtime
(`MONO_WASM: … mono_wasm_start_deputy_thread_async() failed` / `Aborted`).
It is timing‑dependent: it can take anywhere from a few seconds to a few minutes of timer activity to surface.
`Lock.Exit` throws `SynchronizationLockException` when the lock is released by a thread that does not own it,
so this indicates the portable `TimerQueue` lock is being acquired/released across threads under the
multithreaded‑WASM thread pool — independent of user code.
This was first hit in a large app that uses several timers (a periodic render loop, animation, idle caches);
the reproduction below strips it to nothing but timers to isolate it to the runtime.
### Reproduction Steps
Minimal standalone Blazor WebAssembly app. No dependencies beyond the WASM SDK.
**`Repro.csproj`**
```xml
net10.0
enable
true
```
**`Pages/Home.razor`** (the entire repro — fire several timers on the pool):
```razor
@page "/"
@using System.Threading
Timer fires: @_fires
@if (_crash is not null) {
@_crash}
@code {
private long _fires;
private string? _crash;
private readonly List _timers = new();
protected override void OnInitialized()
{
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
{ _crash = (e.ExceptionObject as Exception)?.ToString(); InvokeAsync(StateHasChanged); };
for (int i = 0; i < 8; i++)
_timers.Add(new Timer(_ => Interlocked.Increment(ref _fires), null, dueTime: 1, period: 2));
_ = Refresh();
}
private async Task Refresh()
{
while (true) { StateHasChanged(); await Task.Delay(250); }
}
}
```
1. `dotnet run`
2. Open the printed `https://localhost:/` in a Chromium browser (cross‑origin isolated; the dev server
sets COOP/COEP for threads).
3. Leave the page running. The fire counter increases, then the runtime aborts (intermittently, seconds→minutes).
### Expected behavior
Timers continue firing indefinitely; no exception.
### Actual behavior
Fatal, unhandled exception that aborts the runtime:
```
[ERROR] FATAL UNHANDLED EXCEPTION: System.Threading.SynchronizationLockException: Lock_Exit_SynchronizationLockException
at System.Threading.Lock.Exit(ThreadId currentThreadId)
at System.Threading.TimerQueueTimer.Fire(Boolean isThreadPool)
at System.Threading.TimerQueueTimer.System.Threading.IThreadPoolWorkItem.Execute()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
at System.Threading.Thread.StartCallback()
```
An earlier occurrence showed the same `Lock.Exit` fault via `TimerQueue.SetTimerPortable` → `FireNextTimers`.
### Regression?
Multithreading in Blazor WebAssembly was not supported before .NET 10 (see #116251), so there is no prior
GA baseline to regress from. It is reproducible on the latest 10.0.x.
### Known Workarounds
_No response_
### Configuration
- .NET SDK: **10.0.300**
- Runtime pack: **`Microsoft.NETCore.App.Runtime.Mono.multithread.browser-wasm` 10.0.9** (latest 10.0.x;
reproduces on the newest servicing release available for the 10.0 SDK)
- TFM `net10.0`, Blazor WebAssembly standalone, `WasmEnableThreads=true`, interpreter (no AOT), no native references
- OS: Windows 11
- Browser: Microsoft Edge (Chromium) ``
- `crossOriginIsolated === true`
### Other information
- The fault is in the runtime's portable `TimerQueue`/`System.Threading.Lock`, not user code (the repro has none).
- Related: experimental WASM multithreading tracking #68162; `System.Threading.Lock` #34812; `TimerQueueTimer`
lock contention #9114 / #8646; Blazor‑WASM threading enablement #116251.
Contributor guide
Assessment
This issue has not been assessed yet.