stride3d/stride · error · InvalidOperationException
Trying to dispose a lock that has already been released.
Error message
Trying to dispose a lock that has already been released.
What it means
MicroThreadLock tracks a reentrancy counter; Dispose is only valid while the lock is held at least once. When reentrancy is already 0, the lock has been fully released and disposing again is a programming error, so an InvalidOperationException is thrown. This protects the internal queue of waiting locks from being corrupted by a double-release.
Solutions
- Ensure Dispose is called exactly once per acquired lock, ideally via a single using block.
- Remove redundant manual Dispose calls when the lock is already managed by using or try/finally.
- Guard reentrancy in your own code: only dispose the lock on the path that actually acquired it.
- Wrap Dispose in try/catch (InvalidOperationException) only if the lock lifecycle is genuinely ambiguous.
Example fix
// before
var l = new SyncLock();
l.Lock();
l.Dispose();
l.Dispose(); // throws
// after
using (l.Lock())
{
// work
} // single deterministic dispose Defensive patterns
Strategy: try-catch
Validate before calling
// Track ownership yourself
bool disposed = false;
if (!disposed) { lockObj.Dispose(); disposed = true; } Type guard
bool CanDispose(MicroThreadLock l) => l.Reentrancy > 0; // if exposed via property/internals
Try / catch
try { lockObj.Dispose(); }
catch (InvalidOperationException ex) when (ex.Message.Contains("already been released")) { /* already disposed: log/no-op */ } Prevention
- Use a single using block per acquisition
- Never call Dispose manually on locks managed by using
- Assign lock ownership to one code path
When it happens
Trigger: Calling Dispose() on a MicroThreadLock twice, or calling Dispose() after the lock was released via another path (e.g. the final reentrancy count was decremented elsewhere). Also happens if the same IDisposable is disposed by both a using scope and a manual Dispose call.
Common situations: Double-dispose patterns: wrapping the lock in a using block and also calling Dispose manually in a finally block; shared lock instances disposed by two owners; disposing a lock whose Acquire task failed or was never awaited so reentrancy never incremented.
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
- The first lock in the queue was not the current lock
- 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.
- Trying to unlock while another thread owns the lock.
AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14).
Data as JSON: /api/errors/b3bf7e8f1701c5ae.
Report an issue: GitHub.
Appendix: source
Thrown at sources/core/Stride.Core.Design/MicroThreadLock.cs:120
private abstract class MicroThreadLockBase : IDisposable
{
protected readonly MicroThreadLock MicroThreadLock;
private readonly TaskCompletionSource<int> acquisition;
private int reentrancy;
protected MicroThreadLockBase(MicroThreadLock microThreadLock)
{
MicroThreadLock = microThreadLock;
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();
}
}
}View on GitHub (pinned to 96fad776d2)