stride3d/stride · error · InvalidOperationException

Trying to lock while another thread owns the lock.

Error message

Trying to lock while another thread owns the lock.

What it means

SyncLock.Take assigns the current managed thread id to the static currentSyncLockThread, establishing single-thread ownership of the sync lock. If another thread already owns it (currentSyncLockThread != 0), taking it again would silently steal ownership, so it throws InvalidOperationException.

Solutions

  1. Serialize access yourself (e.g. SemaphoreSlim(1,1) or a monitor) so only one thread calls Lock() at a time.
  2. Ensure the owning thread's using scope exits before other threads attempt Lock().
  3. Use the queued MicroThreadLock variant instead of SyncLock if cross-thread waiting is needed.
  4. Wrap Lock() in try/catch (InvalidOperationException) to detect cross-thread contention and retry later.

Example fix

// before
Parallel.For(0, 10, i => { using (syncLock.Lock()) { /* work */ } }); // throws
// after
var gate = new SemaphoreSlim(1, 1);
Parallel.For(0, 10, async i => { await gate.WaitAsync(); try { using (syncLock.Lock()) { /* work */ } } finally { gate.Release(); } });
Defensive patterns

Strategy: try-catch

Validate before calling

// Only call Lock() on the owning thread, or gate access:
if (SyncLockIsFree()) syncLock.Lock(); // expose/track currentSyncLockThread via your own state

Try / catch

try { using (syncLock.Lock()) { /* work */ } }
catch (InvalidOperationException ex) when (ex.Message.Contains("another thread owns the lock")) { /* defer or retry on owning thread */ }

Prevention

When it happens

Trigger: Calling Take (via Lock()) from a second thread while a first thread still holds the sync lock; concurrent Lock() calls on the same SyncLock from different threads without waiting for release.

Common situations: Background threads or ThreadPool work items entering a Lock() section while the UI/main thread holds it; parallel loops touching the same SyncLock; forgetting that SyncLock is single-owner, unlike the queued MicroThreadLock.

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

Appendix: source

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

        {
        }

        public override void Dispose()
        {
            Monitor.Exit(MicroThreadLock.syncLock);
            base.Dispose();
        }

        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)
            {

View on GitHub (pinned to 96fad776d2)