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
- Keep the entire Lock() using-scope on the acquiring thread: avoid awaits inside it or use the same synchronization context.
- Do not pass the lock's IDisposable to other threads for disposal.
- Use await ... with a captured context, or restructure so disposal happens synchronously on the owner thread.
- 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
- No awaits inside Lock() scopes, or keep the same sync context
- Never pass the Lock() IDisposable across threads
- Dispose in the same call stack that acquired
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
- Trying to lock while another thread owns the lock.
- Trying to dispose a lock that has already been released.
- 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
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)