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
- Serialize access yourself (e.g. SemaphoreSlim(1,1) or a monitor) so only one thread calls Lock() at a time.
- Ensure the owning thread's using scope exits before other threads attempt Lock().
- Use the queued MicroThreadLock variant instead of SyncLock if cross-thread waiting is needed.
- 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
- Serialize Lock() calls across threads (SemaphoreSlim/monitor)
- Keep the lock scope on one thread
- Use the queued MicroThreadLock for cross-thread scenarios
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
- Trying to enter a lock that has already been entered
- Trying to unlock while another thread owns the lock.
- Aynchronous lock can only be acquired from a micro-thread…
- Trying to dispose a lock that has already been released.
- The first lock in the queue was not the current lock
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)