{"record":{"id":"d3cdcc0b6aa44c48","repo":"stride3d/stride","slug":"aynchronous-lock-can-only-be-acquired-from-a-micro-thread","errorCode":null,"errorMessage":"Aynchronous lock can only be acquired from a micro-thread. Use ReserveSyncLock.","messagePattern":"Aynchronous lock can only be acquired from a micro-thread\\. Use ReserveSyncLock\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"sources/core/Stride.Core.Design/MicroThreadLock.cs","lineNumber":63,"sourceCode":"        }\n\n        // Select the proper type of lock depending on whether we're in a micro-thread or not.\n        var newLock = new MicroThreadSyncLock(this);\n        AcquireOrEnqueue(newLock);\n        await newLock.Acquired;\n\n        // In the case of sync, we need to register in the proper thread, so call to Register() is defered to later.\n        return newLock;\n    }\n\n    /// <summary>\n    /// Acquires an asynchronous lock. The lock will be tied to the current <see cref=\"MicroThread\"/> to allow re-entrancy.\n    /// </summary>\n    /// <returns>A task that completes when the lock is acquired.</returns>\n    /// <remarks>This way of acquiring the lock is only valid when in a <see cref=\"MicroThread\"/>.</remarks>\n    public async Task<IDisposable> LockAsync()\n    {\n        if (Scheduler.CurrentMicroThread == null) throw new InvalidOperationException($\"Aynchronous lock can only be acquired from a micro-thread. Use {nameof(ReserveSyncLock)}.\");\n#if NET7_0_OR_GREATER\n        ObjectDisposedException.ThrowIf(isDisposed, this);\n#else\n        if (isDisposed) throw new ObjectDisposedException(nameof(MicroThreadLock));\n#endif\n\n        // If we already acquired the lock in this micro-thread, we're just re-entering\n        if (asyncLocks.IsValueCreated && asyncLocks.Value != null)\n        {\n            var currentLock = asyncLocks.Value;\n            currentLock.Reenter();\n            return currentLock;\n        }\n\n        // Select the proper type of lock depending on whether we're in a micro-thread or not.\n        var newLock = new MicroThreadAsyncLock(this);\n        AcquireOrEnqueue(newLock);\n        await newLock.Acquired;","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/stride3d/stride/blob/96fad776d210c221682aac1ccdf4c79dc046fc38/sources/core/Stride.Core.Design/MicroThreadLock.cs#L45-L81","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\npublic async Task DoWork(MicroThreadLock l)\n{\n    await using var h = await l.LockAsync(); // throws outside micro-thread\n}\n// after\nvar handle = l.ReserveSyncLock();\nlock (handle) { /* work */ }","handlingStrategy":"try-catch","validationCode":"if (Scheduler.CurrentMicroThread == null)\n    throw new InvalidOperationException(\"LockAsync requires a micro-thread; use ReserveSyncLock.\");","typeGuard":null,"tryCatchPattern":"try { var h = await lockObj.LockAsync(); }\ncatch (InvalidOperationException) { /* fall back to ReserveSyncLock */ }\ncatch (ObjectDisposedException) { /* lock disposed */ }","preventionTips":["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"],"tags":["csharp","concurrency","microthreads"],"backgroundTag":"unsupported-operation","analyzedSha":"96fad776d210c221682aac1ccdf4c79dc046fc38","analyzedAt":"2026-09-14T02:59:31.279Z","contentChangedAt":"2026-09-14T02:59:31.279Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}