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
- Use a fresh MicroThreadLock instance for each independent acquisition.
- Let the built-in Lock()/queue mechanism manage acquisition instead of calling Acquire directly.
- Ensure the previous holder fully Disposed/released the lock before reacquiring.
- 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
- Use Lock() scopes rather than raw Acquire
- Create a new lock instance per independent wait
- Ensure prior holders released before reacquiring
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
- Trying to reenter a lock that has not yet been acquired
- Trying to lock while another thread owns the lock.
- Aynchronous lock can only be acquired from a micro-thread…
- Trying to dispose a lock that has already been released.
- The first lock in the queue was not the current lock
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)