{"record":{"id":"e1d694ee0b87c85c","repo":"stride3d/stride","slug":"a-dispatcher-lock-must-be-created-from-a-different-thread","errorCode":null,"errorMessage":"A dispatcher lock must be created from a different thread that the dispatchers it should lock","messagePattern":"A dispatcher lock must be created from a different thread that the dispatchers it should lock","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Services/DispatcherLock.cs","lineNumber":24,"sourceCode":"using System.Threading.Tasks;\nusing Stride.Core.Annotations;\nusing Stride.Core.Extensions;\nusing Stride.Core.Presentation.Services;\n\nnamespace Stride.Assets.Presentation.AssetEditors.GameEditor.Services\n{\n    /// <summary>\n    /// An object that allows to lock several dispatchers at the same-time to perform an operation in a thread-safe way.\n    /// </summary>\n    public class DispatcherLock : IDisposable\n    {\n        // TODO: we might want to move that to the plugin level at some point (or even above? in Presentation?)\n        private struct DispatcherState\n        {\n            public DispatcherState([NotNull] IDispatcherService dispatcher)\n            {\n                if (dispatcher == null) throw new ArgumentNullException(nameof(dispatcher));\n                if (dispatcher.CheckAccess()) throw new InvalidOperationException(\"A dispatcher lock must be created from a different thread that the dispatchers it should lock\");\n                Dispatcher = dispatcher;\n                Locked = new TaskCompletionSource<int>();\n                Unlocked = new TaskCompletionSource<int>();\n            }\n\n            public readonly IDispatcherService Dispatcher;\n            public readonly TaskCompletionSource<int> Locked;\n            public readonly TaskCompletionSource<int> Unlocked;\n        }\n\n        private readonly List<DispatcherState> dispatcherStates;\n\n        /// <summary>\n        /// Initializes a new instance of the <see cref=\"DispatcherLock\"/> class.\n        /// </summary>\n        /// <param name=\"dispatchers\">The dispatchers to lock.</param>\n        private DispatcherLock([ItemNotNull, NotNull] IEnumerable<IDispatcherService> dispatchers)\n        {","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/stride3d/stride/blob/96fad776d210c221682aac1ccdf4c79dc046fc38/sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Services/DispatcherLock.cs#L6-L42","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Call DispatcherLock.Lock from a background thread, e.g. Task.Run(() => DispatcherLock.Lock(...)) or inside an InvokeAsync on another dispatcher.","Add ConfigureAwait(false) to awaited calls before Lock so continuations do not resume on the UI thread.","Restructure the code to dispatch UI work INTO the locked dispatcher rather than taking the lock from the dispatcher thread."],"exampleFix":"// before (running on UI thread)\nusing (await DispatcherLock.Lock(true, controller, dispatcher)) { ... }\n// after\nawait Task.Run(async () =>\n{\n    using (await DispatcherLock.Lock(true, controller, dispatcher)) { ... }\n});","handlingStrategy":"try-catch","validationCode":"if (dispatcher.CheckAccess())\n    throw new InvalidOperationException(\"Do not take a DispatcherLock from the dispatcher thread; hop to a background thread first.\");","typeGuard":null,"tryCatchPattern":"try { using (await DispatcherLock.Lock(true, controller, dispatcher)) { /* ... */ } }\ncatch (InvalidOperationException) { await Task.Run(async () => { using (await DispatcherLock.Lock(true, controller, dispatcher)) { /* ... */ } }); }","preventionTips":["Never take dispatcher locks from UI event handlers directly","Use ConfigureAwait(false) in library-style code before acquiring locks","Prefer InvokeAsync into the dispatcher over locking from it"],"tags":["deadlock","threading","dispatcher","wpf"],"backgroundTag":"invalid-state-transition","analyzedSha":"96fad776d210c221682aac1ccdf4c79dc046fc38","analyzedAt":"2026-09-14T02:59:31.279Z","contentChangedAt":"2026-09-14T02:59:31.279Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}