stride3d/stride · error · InvalidOperationException

A dispatcher lock must be created from a different thread…

Error message

A dispatcher lock must be created from a different thread that the dispatchers it should lock

What it means

DispatcherLock's DispatcherState acquires an exclusive lock over a dispatcher's message loop. It validates that the constructor is running on a thread OTHER than the dispatcher's own thread (dispatcher.CheckAccess() must be false) — otherwise the code creating the lock would deadlock waiting on tasks that must be pumped by the very thread it is blocking.

Solutions

  1. Call DispatcherLock.Lock from a background thread, e.g. Task.Run(() => DispatcherLock.Lock(...)) or inside an InvokeAsync on another dispatcher.
  2. Add ConfigureAwait(false) to awaited calls before Lock so continuations do not resume on the UI thread.
  3. Restructure the code to dispatch UI work INTO the locked dispatcher rather than taking the lock from the dispatcher thread.

Example fix

// before (running on UI thread)
using (await DispatcherLock.Lock(true, controller, dispatcher)) { ... }
// after
await Task.Run(async () =>
{
    using (await DispatcherLock.Lock(true, controller, dispatcher)) { ... }
});
Defensive patterns

Strategy: try-catch

Validate before calling

if (dispatcher.CheckAccess())
    throw new InvalidOperationException("Do not take a DispatcherLock from the dispatcher thread; hop to a background thread first.");

Try / catch

try { using (await DispatcherLock.Lock(true, controller, dispatcher)) { /* ... */ } }
catch (InvalidOperationException) { await Task.Run(async () => { using (await DispatcherLock.Lock(true, controller, dispatcher)) { /* ... */ } }); }

Prevention

When it happens

Trigger: Calling DispatcherLock.Lock(...) from code already executing on the dispatcher thread being locked (e.g. inside a UI event handler or an async continuation resumed on the UI thread) with an IDispatcherService whose CheckAccess() returns true.

Common situations: Calling Lock from a WPF button click handler; invoking Lock withoutConfigureAwait(false) so an async continuation resumes on the captured UI dispatcher; plugin code that assumes it runs on a background thread.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/e1d694ee0b87c85c. Report an issue: GitHub.

Appendix: source

Thrown at sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Services/DispatcherLock.cs:24

using System.Threading.Tasks;
using Stride.Core.Annotations;
using Stride.Core.Extensions;
using Stride.Core.Presentation.Services;

namespace Stride.Assets.Presentation.AssetEditors.GameEditor.Services
{
    /// <summary>
    /// An object that allows to lock several dispatchers at the same-time to perform an operation in a thread-safe way.
    /// </summary>
    public class DispatcherLock : IDisposable
    {
        // TODO: we might want to move that to the plugin level at some point (or even above? in Presentation?)
        private struct DispatcherState
        {
            public DispatcherState([NotNull] IDispatcherService dispatcher)
            {
                if (dispatcher == null) throw new ArgumentNullException(nameof(dispatcher));
                if (dispatcher.CheckAccess()) throw new InvalidOperationException("A dispatcher lock must be created from a different thread that the dispatchers it should lock");
                Dispatcher = dispatcher;
                Locked = new TaskCompletionSource<int>();
                Unlocked = new TaskCompletionSource<int>();
            }

            public readonly IDispatcherService Dispatcher;
            public readonly TaskCompletionSource<int> Locked;
            public readonly TaskCompletionSource<int> Unlocked;
        }

        private readonly List<DispatcherState> dispatcherStates;

        /// <summary>
        /// Initializes a new instance of the <see cref="DispatcherLock"/> class.
        /// </summary>
        /// <param name="dispatchers">The dispatchers to lock.</param>
        private DispatcherLock([ItemNotNull, NotNull] IEnumerable<IDispatcherService> dispatchers)
        {

View on GitHub (pinned to 96fad776d2)