stride3d/stride · error · InvalidOperationException
The first lock in the queue was not the current lock
Error message
The first lock in the queue was not the current lock
What it means
When the last reentrancy is released, MicroThreadLock removes itself from a shared FIFO lockQueue. The dequeued lock must be the instance calling Dispose; if it is not, the shared queue has been mutated out of order, which is an internal invariant violation, so it throws InvalidOperationException.
Solutions
- Release locks in strictly reverse order of acquisition (LIFO), keeping each Lock() in a using block.
- Do not hoist IDisposable lock handles out of their acquisition scope.
- Audit nested lock usages after refactors so inner scopes still exit first.
- Treat this as a bug: the queue invariant is broken, fix ownership/ordering rather than catching.
Example fix
// before
var outer = lock1.Lock();
var inner = lock2.Lock();
outer.Dispose(); // out of order
inner.Dispose();
// after
using (lock1.Lock())
using (lock2.Lock())
{
// work
} // inner disposes first Defensive patterns
Strategy: validation
Validate before calling
// Release in reverse acquisition order (LIFO); keep handles in a stack var stack = new Stack<IDisposable>(); stack.Push(lockA.Lock()); stack.Push(lockB.Lock()); // on exit: while (stack.Count > 0) stack.Pop().Dispose();
Try / catch
try { current.Dispose(); }
catch (InvalidOperationException ex) when (ex.Message.Contains("first lock in the queue")) { /* ordering bug: fix scopes, do not swallow in production */ } Prevention
- Always release in reverse acquisition order
- Keep Lock() handles inside their acquisition using scope
- Avoid reordering using blocks during refactors
When it happens
Trigger: Disposing locks out of the order they were enqueued, e.g. nested Lock() scopes whose inner using block exits before the outer one's, or disposing a lock instance from a different order/timing than acquisition.
Common situations: Nested using blocks re-ordered by early returns or exceptions in refactored code; multiple threads manipulating the static lockQueue concurrently; moving Dispose calls into a different method so stack order is broken.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- Internal error, the collection has already muted to a…
- Trying to dispose a lock that has already been released.
- Trying to enter a lock that has already been entered
- Trying to reenter a lock that has not yet been acquired
- Trying to lock while another thread owns the lock.
AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14).
Data as JSON: /api/errors/049eedf44c452298.
Report an issue: GitHub.
Appendix: source
Thrown at sources/core/Stride.Core.Design/MicroThreadLock.cs:130
acquisition = new TaskCompletionSource<int>();
}
public Task Acquired => acquisition.Task;
public virtual void Dispose()
{
if (reentrancy == 0)
throw new InvalidOperationException("Trying to dispose a lock that has already been released.");
--reentrancy;
if (reentrancy == 0)
{
Release();
lock (MicroThreadLock.lockQueue)
{
// Remove ourself from the queue.
var thisLock = MicroThreadLock.lockQueue.Dequeue();
if (thisLock != this) throw new InvalidOperationException("The first lock in the queue was not the current lock");
// If another lock is waiting, let's acquire it
if (MicroThreadLock.lockQueue.Count > 0)
{
var nextLock = MicroThreadLock.lockQueue.Peek();
nextLock.Acquire();
}
}
}
}
internal void Acquire()
{
if (reentrancy != 0) throw new InvalidOperationException("Trying to enter a lock that has already been entered");
++reentrancy;
acquisition.SetResult(0);
}
internal virtual void Reenter()View on GitHub (pinned to 96fad776d2)