// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using Stride.Core.MicroThreading; namespace Stride.Core; /// /// An hybrid lock that allows to do asynchrounous work when acquired from a , and still allow to await for acquisition out of a /// microthread. This lock support re-entrancy. /// public class MicroThreadLock : IDisposable { private readonly MicroThreadLocal asyncLocks = new(); private readonly Queue lockQueue = []; private readonly object syncLock = new(); private int currentSyncLockThread; private MicroThreadSyncLock currentSyncLock; private bool isDisposed; /// public void Dispose() { isDisposed = true; } /// /// Reserves the lock in order to use synchronous locking. The lock will be bound to the calling thread and therefore should be released on the same thread. /// /// A task that completes when the lock is reserved. The result of the task is an object allowing to do the lock. public async Task 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 thread, we're just re-entering if (currentSyncLockThread == Environment.CurrentManagedThreadId) { var currentLock = currentSyncLock; // Reentering will happen in the Lock() method return currentLock; } // 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; } /// /// Acquires an asynchronous lock. The lock will be tied to the current to allow re-entrancy. /// /// A task that completes when the lock is acquired. /// This way of acquiring the lock is only valid when in a . public async Task 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; // In the case of async we can register immediately after acquiring. For sync, we need to register in the proper thread. newLock.Register(); return newLock; } private void AcquireOrEnqueue(MicroThreadLockBase lockToAcquire) { lock (lockQueue) { if (lockQueue.Count == 0) { // Nothing else is in the queue, let's acquire immediately. lockToAcquire.Acquire(); } // Let's enqueue this new lock so it can be notified by the previous lock when it can be acquired. lockQueue.Enqueue(lockToAcquire); } } private abstract class MicroThreadLockBase : IDisposable { protected readonly MicroThreadLock MicroThreadLock; private readonly TaskCompletionSource acquisition; private int reentrancy; protected MicroThreadLockBase(MicroThreadLock microThreadLock) { MicroThreadLock = microThreadLock; acquisition = new TaskCompletionSource(); } public Task Acquired => acquisition.Task; public virtual void Dispose() { if (reentrancy == 0) throw new InvalidOperationException("Trying to dispose a lock that has already been released."); --reentrancy; if (reentrancy == 0) { Release(); lock (MicroThreadLock.lockQueue) { // Remove ourself from the queue. var thisLock = MicroThreadLock.lockQueue.Dequeue(); if (thisLock != this) throw new InvalidOperationException("The first lock in the queue was not the current lock"); // If another lock is waiting, let's acquire it if (MicroThreadLock.lockQueue.Count > 0) { var nextLock = MicroThreadLock.lockQueue.Peek(); nextLock.Acquire(); } } } } internal void Acquire() { if (reentrancy != 0) throw new InvalidOperationException("Trying to enter a lock that has already been entered"); ++reentrancy; acquisition.SetResult(0); } internal virtual void Reenter() { if (!acquisition.Task.IsCompleted) throw new InvalidOperationException("Trying to reenter a lock that has not yet been acquired"); ++reentrancy; } internal abstract void Release(); } private class MicroThreadAsyncLock : MicroThreadLockBase { public MicroThreadAsyncLock(MicroThreadLock microThreadLock) : base(microThreadLock) { } internal void Register() { MicroThreadLock.asyncLocks.Value = this; } internal override void Release() { MicroThreadLock.asyncLocks.Value = null; } } private class MicroThreadSyncLock : MicroThreadLockBase, ISyncLockable { private bool locked; public MicroThreadSyncLock(MicroThreadLock microThreadLock) : base(microThreadLock) { } 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) { // We register here because we are in the proper thread. Take(); Monitor.Enter(MicroThreadLock.syncLock); } else { Reenter(); } locked = true; return this; } } }