Description
In a multithreaded Blazor WebAssembly app (<WasmEnableThreads>true</WasmEnableThreads>), 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
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<WasmEnableThreads>true</WasmEnableThreads>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all" />
</ItemGroup>
</Project>
Pages/Home.razor (the entire repro — fire several timers on the pool):
@page "/"
@using System.Threading
<h3>Timer fires: @_fires</h3>
@if (_crash is not null) { <pre>@_crash</pre> }
@code {
private long _fires;
private string? _crash;
private readonly List<Timer> _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); }
}
}
dotnet run
- Open the printed
https://localhost:<port>/ in a Chromium browser (cross‑origin isolated; the dev server
sets COOP/COEP for threads).
- 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)
<version>
crossOriginIsolated === true
Other information
Description
In a multithreaded Blazor WebAssembly app (
<WasmEnableThreads>true</WasmEnableThreads>), usingSystem.Threading.Timerleads to an intermittent, fatalSystem.Threading.SynchronizationLockExceptionthrown from inside the runtime's own timer queue —
System.Threading.Lock.Exitcalled fromTimerQueueTimer.Fireon 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.ExitthrowsSynchronizationLockExceptionwhen the lock is released by a thread that does not own it,so this indicates the portable
TimerQueuelock is being acquired/released across threads under themultithreaded‑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.csprojPages/Home.razor(the entire repro — fire several timers on the pool):dotnet runhttps://localhost:<port>/in a Chromium browser (cross‑origin isolated; the dev serversets COOP/COEP for threads).
Expected behavior
Timers continue firing indefinitely; no exception.
Actual behavior
Fatal, unhandled exception that aborts the runtime:
An earlier occurrence showed the same
Lock.Exitfault viaTimerQueue.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
Microsoft.NETCore.App.Runtime.Mono.multithread.browser-wasm10.0.9 (latest 10.0.x;reproduces on the newest servicing release available for the 10.0 SDK)
net10.0, Blazor WebAssembly standalone,WasmEnableThreads=true, interpreter (no AOT), no native references<version>crossOriginIsolated === trueOther information
TimerQueue/System.Threading.Lock, not user code (the repro has none).System.Threading.LockAdd first class System.Threading.Lock type #34812;TimerQueueTimerlock contention Fix the Scalability problem in System.Threading.TimerQueueTimer #9114 / Lock convoy on TimerQueueTimer #8646; Blazor‑WASM threading enablement Setting WasmEnableThreads to true on .NET 9.0 WebAssembly dev server should tell you it's not going to work #116251.