stride3d/stride · error · InvalidOperationException

Trying to unlock while another thread owns the lock.

Error message

Trying to unlock while another thread owns the lock.

What it means

SyncLock.Release verifies that the thread calling it is the same thread that currently owns the sync lock (currentSyncLockThread). If a different thread attempts to release, ownership bookkeeping would be corrupted, so it throws InvalidOperationException.

Solutions

  1. Keep the entire Lock() using-scope on the acquiring thread: avoid awaits inside it or use the same synchronization context.
  2. Do not pass the lock's IDisposable to other threads for disposal.
  3. Use await ... with a captured context, or restructure so disposal happens synchronously on the owner thread.
  4. Check Environment.CurrentManagedThreadId at acquisition and disposal when debugging ownership issues.

Example fix

// before
using (syncLock.Lock())
{
    await Task.Run(Work); // continuation may release on another thread
}
// after
using (syncLock.Lock())
{
    Work(); // keep scope synchronous on owning thread
}
await Task.Run(Work);
Defensive patterns

Strategy: try-catch

Validate before calling

// Before disposing, confirm thread affinity:
var ownerThread = acquireThreadId; // recorded at Lock() time
bool canRelease = ownerThread == Environment.CurrentManagedThreadId;

Type guard

bool CanRelease(int acquiringThreadId) => acquiringThreadId == Environment.CurrentManagedThreadId;

Try / catch

try { lockHandle.Dispose(); }
catch (InvalidOperationException ex) when (ex.Message.Contains("another thread owns the lock")) { /* marshal disposal back to the owning thread */ }

Prevention

When it happens

Trigger: Disposing the IDisposable returned by Lock() on a thread other than the acquiring thread — e.g. passing the IDisposable across threads, marshaling disposal to the UI thread, or using async void/continuations that resume on a different thread.

Common situations: await inside a Lock() scope with ConfigureAwait(false) or no sync context, so the using's Dispose runs on a ThreadPool thread; queueing Dispose to a dispatcher; leaking the IDisposable to another thread.

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

Appendix: source

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

        internal override void Reenter()
        {
            Monitor.Enter(MicroThreadLock.syncLock);
            base.Reenter();
        }

        internal void Take()
        {
            if (MicroThreadLock.currentSyncLockThread != 0)
                throw new InvalidOperationException("Trying to lock while another thread owns the lock.");

            MicroThreadLock.currentSyncLockThread = Environment.CurrentManagedThreadId;
            MicroThreadLock.currentSyncLock = this;
        }

        internal override void Release()
        {
            if (MicroThreadLock.currentSyncLockThread != Environment.CurrentManagedThreadId)
                throw new InvalidOperationException("Trying to unlock while another thread owns the lock.");

            MicroThreadLock.currentSyncLockThread = 0;
            MicroThreadLock.currentSyncLock = null;
        }

        public IDisposable Lock()
        {
            if (!locked)
            {
                // We register here because we are in the proper thread.
                Take();
                Monitor.Enter(MicroThreadLock.syncLock);
            }
            else
            {
                Reenter();
            }
            locked = true;

View on GitHub (pinned to 96fad776d2)