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

  1. Release locks in strictly reverse order of acquisition (LIFO), keeping each Lock() in a using block.
  2. Do not hoist IDisposable lock handles out of their acquisition scope.
  3. Audit nested lock usages after refactors so inner scopes still exit first.
  4. 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

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


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)