stride3d/stride · error · InvalidOperationException

Trying to enter a lock that has already been entered

Error message

Trying to enter a lock that has already been entered

What it means

Acquire() sets the lock's reentrancy counter to 1 and completes the acquisition task. If reentrancy is already non-zero the lock is currently held, and acquiring it again from a fresh context would corrupt the ownership model, so an InvalidOperationException is thrown.

Solutions

  1. Use a fresh MicroThreadLock instance for each independent acquisition.
  2. Let the built-in Lock()/queue mechanism manage acquisition instead of calling Acquire directly.
  3. Ensure the previous holder fully Disposed/released the lock before reacquiring.
  4. If lock contention is intended, enqueue the lock rather than calling Acquire on a held instance.

Example fix

// before
var l = new SyncLock();
l.AcquireOrEnqueue();
l.AcquireOrEnqueue(); // throws on second use
// after
using (l.Lock()) { /* work */ }
var l2 = new SyncLock();
using (l2.Lock()) { /* next work */ }
Defensive patterns

Strategy: validation

Validate before calling

// only acquire when not held
if (lockObj.Reentrancy == 0) lockObj.Acquire(); // or rely on Lock()/queue mechanism

Type guard

bool CanAcquire(MicroThreadLock l) => l.Reentrancy == 0;

Try / catch

try { lockObj.Acquire(); }
catch (InvalidOperationException ex) when (ex.Message.Contains("already been entered")) { /* already held: enqueue/wait instead */ }

Prevention

When it happens

Trigger: Calling Acquire (via AcquireOrEnqueue) on a lock instance that is already acquired and not yet released — e.g. reusing a single MicroThreadLock object for a second concurrent wait, or the queue logic invoking Acquire on a lock that never released.

Common situations: Sharing one lock instance across microthreads/tasks that overlap; recreating work items that re-use a stale lock object instead of a new one; race conditions in custom scheduler code that bypasses the queue.

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/c472f51e33478660. Report an issue: GitHub.

Appendix: source

Thrown at sources/core/Stride.Core.Design/MicroThreadLock.cs:143

                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()
        {
            if (!acquisition.Task.IsCompleted) throw new InvalidOperationException("Trying to reenter a lock that has not yet been acquired");
            ++reentrancy;
        }

        internal abstract void Release();
    }

    private class MicroThreadAsyncLock : MicroThreadLockBase
    {
        public MicroThreadAsyncLock(MicroThreadLock microThreadLock)
            : base(microThreadLock)
        {

View on GitHub (pinned to 96fad776d2)