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

  1. Ensure Dispose is called exactly once per acquired lock, ideally via a single using block.
  2. Remove redundant manual Dispose calls when the lock is already managed by using or try/finally.
  3. Guard reentrancy in your own code: only dispose the lock on the path that actually acquired it.
  4. 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

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


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)