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

  1. Run the code inside a MicroThread (e.g. via MicroThreadCallback/ScriptRenderer or Scheduler.Run) when using LockAsync
  2. Use ReserveSyncLock to obtain a synchronous lock reservation when not in a micro-thread
  3. 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

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


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)