Skip to content

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

Description

@jimm98y

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); }
    }
}
  1. dotnet run
  2. Open the printed https://localhost:<port>/ 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.SetTimerPortableFireNextTimers.

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

Metadata

Metadata

Assignees

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions