{"record":{"id":"b3bf7e8f1701c5ae","repo":"stride3d/stride","slug":"trying-to-dispose-a-lock-that-has-already-been-released","errorCode":null,"errorMessage":"Trying to dispose a lock that has already been released.","messagePattern":"Trying to dispose a lock that has already been released\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"sources/core/Stride.Core.Design/MicroThreadLock.cs","lineNumber":120,"sourceCode":"\n    private abstract class MicroThreadLockBase : IDisposable\n    {\n        protected readonly MicroThreadLock MicroThreadLock;\n        private readonly TaskCompletionSource<int> acquisition;\n        private int reentrancy;\n\n        protected MicroThreadLockBase(MicroThreadLock microThreadLock)\n        {\n            MicroThreadLock = microThreadLock;\n            acquisition = new TaskCompletionSource<int>();\n        }\n\n        public Task Acquired => acquisition.Task;\n\n        public virtual void Dispose()\n        {\n            if (reentrancy == 0)\n                throw new InvalidOperationException(\"Trying to dispose a lock that has already been released.\");\n\n            --reentrancy;\n            if (reentrancy == 0)\n            {\n                Release();\n                lock (MicroThreadLock.lockQueue)\n                {\n                    // Remove ourself from the queue.\n                    var thisLock = MicroThreadLock.lockQueue.Dequeue();\n                    if (thisLock != this) throw new InvalidOperationException(\"The first lock in the queue was not the current lock\");\n                    // If another lock is waiting, let's acquire it\n                    if (MicroThreadLock.lockQueue.Count > 0)\n                    {\n                        var nextLock = MicroThreadLock.lockQueue.Peek();\n                        nextLock.Acquire();\n                    }\n                }\n            }","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/stride3d/stride/blob/96fad776d210c221682aac1ccdf4c79dc046fc38/sources/core/Stride.Core.Design/MicroThreadLock.cs#L102-L138","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nvar l = new SyncLock();\nl.Lock();\nl.Dispose();\nl.Dispose(); // throws\n// after\nusing (l.Lock())\n{\n    // work\n} // single deterministic dispose","handlingStrategy":"try-catch","validationCode":"// Track ownership yourself\nbool disposed = false;\nif (!disposed) { lockObj.Dispose(); disposed = true; }","typeGuard":"bool CanDispose(MicroThreadLock l) => l.Reentrancy > 0; // if exposed via property/internals","tryCatchPattern":"try { lockObj.Dispose(); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"already been released\")) { /* already disposed: log/no-op */ }","preventionTips":["Use a single using block per acquisition","Never call Dispose manually on locks managed by using","Assign lock ownership to one code path"],"tags":["csharp","lifecycle","double-dispose","locking"],"backgroundTag":"invalid-state-transition","analyzedSha":"96fad776d210c221682aac1ccdf4c79dc046fc38","analyzedAt":"2026-09-14T02:59:31.279Z","contentChangedAt":"2026-09-14T02:59:31.279Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}