dotnet / dotnet/runtime

[browser][mt] Fatal `SynchronizationLockException` from `System.Threading.Lock.Exit` in `TimerQueueTimer.Fire` on multithreaded WebAssembly

Open
#129,900 4 comments 0 reactions 1 assignee Claimed by @pavelsavara View on GitHub
arch-wasm area-System.Threading os-browser
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.