Files
Nicolas Musset 41381dedd5 refactor: Nullable and Modernization for Core projects (#2643)
* [Core.Mathematics] Modernize code

* [Core.Mathematics] Add tests to Stride.Tests.Simple

* [Core] Cleanup some code

* [Core.CompilerServices] Cleanup some code

* [Core.CompilerServices] Add tests to Stride.Tests.Simple

* [Core.IO] Modernize code

* [Core.MicroThreading] Modernize code

* [Core.Serialization] Modernize code

* [Core.AssemblyProcessor.Tests] Modernize code

* [Core.CompilerServices.Tests] Modernize code

* [Core.Mathematics.Tests] Modernize code

* [Core.Tests] Modernize code

* [Core.Translation] Modernize code

* [Core.Reflection] Modernize code

* Fixes build of Stride.VisualStudio.sln

* [Core.Yaml.Tests] Modernize code

* [Core.Design] Modernize code

* [Core.Design.Tests] Modernize code

* [Core.Mathematics] More annotations

* Fix build of projects that import code from Core.Design

Especially those targeting .NET Framework
2025-02-22 16:50:47 +01:00

231 lines
8.0 KiB
C#

// 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;
/// <summary>
/// An hybrid lock that allows to do asynchrounous work when acquired from a <see cref="MicroThread"/>, and still allow to await for acquisition out of a
/// microthread. This lock support re-entrancy.
/// </summary>
public class MicroThreadLock : IDisposable
{
private readonly MicroThreadLocal<MicroThreadAsyncLock> asyncLocks = new();
private readonly Queue<MicroThreadLockBase> lockQueue = [];
private readonly object syncLock = new();
private int currentSyncLockThread;
private MicroThreadSyncLock currentSyncLock;
private bool isDisposed;
/// <inheritdoc/>
public void Dispose()
{
isDisposed = true;
}
/// <summary>
/// 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.
/// </summary>
/// <returns>A task that completes when the lock is reserved. The result of the task is an <see cref="ISyncLockable"/> object allowing to do the lock.</returns>
public async Task<ISyncLockable> 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;
}
/// <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;
// 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<int> acquisition;
private int reentrancy;
protected MicroThreadLockBase(MicroThreadLock microThreadLock)
{
MicroThreadLock = microThreadLock;
acquisition = new TaskCompletionSource<int>();
}
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;
}
}
}