stride3d/stride · error · InvalidOperationException
Aynchronous lock can only be acquired from a micro-thread…
Error message
Aynchronous lock can only be acquired from a micro-thread. Use ReserveSyncLock.
What it means
MicroThreadLock.LockAsync throws InvalidOperationException when called outside of a micro-thread (Scheduler.CurrentMicroThread is null). Async lock acquisition is tied to the current MicroThread for ownership and re-entrancy tracking, so it is only valid inside one. Outside a micro-thread you must use ReserveSyncLock instead. It also throws ObjectDisposedException if the lock has been disposed.
Solutions
- Run the code inside a MicroThread (e.g. via MicroThreadCallback/ScriptRenderer or Scheduler.Run) when using LockAsync
- Use ReserveSyncLock to obtain a synchronous lock reservation when not in a micro-thread
- Restructure so the lock is acquired on the micro-thread before awaiting work off-thread
Example fix
// before
public async Task DoWork(MicroThreadLock l)
{
await using var h = await l.LockAsync(); // throws outside micro-thread
}
// after
var handle = l.ReserveSyncLock();
lock (handle) { /* work */ } Defensive patterns
Strategy: try-catch
Validate before calling
if (Scheduler.CurrentMicroThread == null)
throw new InvalidOperationException("LockAsync requires a micro-thread; use ReserveSyncLock."); Try / catch
try { var h = await lockObj.LockAsync(); }
catch (InvalidOperationException) { /* fall back to ReserveSyncLock */ }
catch (ObjectDisposedException) { /* lock disposed */ } Prevention
- Only await LockAsync from code running as a MicroThread
- Use ReserveSyncLock for ordinary threads/tasks
- Check disposal state before using the lock in long-lived services
When it happens
Trigger: Calling `await lock.LockAsync()` from a plain Task, thread pool thread, async void handler, or unit test not wrapped in a MicroThread — i.e. anywhere Scheduler.CurrentMicroThread is null.
Common situations: Tests invoking lock logic directly without creating a micro-thread, moving game logic onto background tasks, calling the lock from event handlers not running on a micro-thread.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- Trying to enter a lock that has already been entered
- Trying to lock while another thread owns the lock.
- [ ] cannot be null in
- [ . ] must be in
- [ ] cannot be null in
AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14).
Data as JSON: /api/errors/d3cdcc0b6aa44c48.
Report an issue: GitHub.
Appendix: source
Thrown at sources/core/Stride.Core.Design/MicroThreadLock.cs:63
}
// Select the proper type of lock depending on whether we're in a micro-thread or not.
var newLock = new MicroThreadSyncLock(this);
AcquireOrEnqueue(newLock);
await newLock.Acquired;
// In the case of sync, we need to register in the proper thread, so call to Register() is defered to later.
return newLock;
}
/// <summary>
/// Acquires an asynchronous lock. The lock will be tied to the current <see cref="MicroThread"/> to allow re-entrancy.
/// </summary>
/// <returns>A task that completes when the lock is acquired.</returns>
/// <remarks>This way of acquiring the lock is only valid when in a <see cref="MicroThread"/>.</remarks>
public async Task<IDisposable> LockAsync()
{
if (Scheduler.CurrentMicroThread == null) throw new InvalidOperationException($"Aynchronous lock can only be acquired from a micro-thread. Use {nameof(ReserveSyncLock)}.");
#if NET7_0_OR_GREATER
ObjectDisposedException.ThrowIf(isDisposed, this);
#else
if (isDisposed) throw new ObjectDisposedException(nameof(MicroThreadLock));
#endif
// If we already acquired the lock in this micro-thread, we're just re-entering
if (asyncLocks.IsValueCreated && asyncLocks.Value != null)
{
var currentLock = asyncLocks.Value;
currentLock.Reenter();
return currentLock;
}
// Select the proper type of lock depending on whether we're in a micro-thread or not.
var newLock = new MicroThreadAsyncLock(this);
AcquireOrEnqueue(newLock);
await newLock.Acquired;View on GitHub (pinned to 96fad776d2)