// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Diagnostics; using System.Threading; using Microsoft.CodeAnalysis; #if WORKSPACE using Microsoft.CodeAnalysis.Internal.Log; #endif namespace Roslyn.Utilities; /// <summary> /// A lightweight mutual exclusion object which supports waiting with cancellation and prevents /// recursion (i.e. you may not call Wait if you already hold the lock) /// </summary> /// <remarks> /// <para> /// The <see cref="NonReentrantLock"/> provides a lightweight mutual exclusion class that doesn't /// use Windows kernel synchronization primitives. /// </para> /// <para> /// The implementation is distilled from the workings of <see cref="SemaphoreSlim"/> /// The basic idea is that we use a regular sync object (Monitor.Enter/Exit) to guard the setting /// of an 'owning thread' field. If, during the Wait, we find the lock is held by someone else /// then we register a cancellation callback and enter a "Monitor.Wait" loop. If the cancellation /// callback fires, then it "pulses" all the waiters to wake them up and check for cancellation. /// Waiters are also "pulsed" when leaving the lock. /// </para> /// <para> /// All public members of <see cref="NonReentrantLock"/> are thread-safe and may be used concurrently /// from multiple threads. /// </para> /// </remarks> internal sealed class NonReentrantLock { /// <summary> /// A synchronization object to protect access to the <see cref="_owningThreadId"/> field and to be pulsed /// when <see cref="Release"/> is called and during cancellation. /// </summary> private readonly object _syncLock; /// <summary> /// The <see cref="Environment.CurrentManagedThreadId" /> of the thread that holds the lock. Zero if no thread is holding /// the lock. /// </summary> private volatile int _owningThreadId; /// <summary> /// Constructor. /// </summary> /// <param name="useThisInstanceForSynchronization">If false (the default), then the class /// allocates an internal object to be used as a sync lock. /// If true, then the sync lock object will be the NonReentrantLock instance itself. This /// saves an allocation but a client may not safely further use this instance in a call to /// Monitor.Enter/Exit or in a "lock" statement. /// </param> public NonReentrantLock(bool useThisInstanceForSynchronization = false) => _syncLock = useThisInstanceForSynchronization ? this : new object(); /// <summary> /// Shared factory for use in lazy initialization. /// </summary> public static readonly Func<NonReentrantLock> Factory = () => new NonReentrantLock(useThisInstanceForSynchronization: true); /// <summary> /// Blocks the current thread until it can enter the <see cref="NonReentrantLock"/>, while observing a /// <see cref="CancellationToken"/>. /// </summary> /// <remarks> /// Recursive locking is not supported. i.e. A thread may not call Wait successfully twice without an /// intervening <see cref="Release"/>. /// </remarks> /// <param name="cancellationToken">The <see cref="CancellationToken"/> token to /// observe.</param> /// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> was /// canceled.</exception> /// <exception cref="LockRecursionException">The caller already holds the lock</exception> public void Wait(CancellationToken cancellationToken = default) { if (this.IsOwnedByMe) { throw new LockRecursionException(); } CancellationTokenRegistration cancellationTokenRegistration = default; if (cancellationToken.CanBeCanceled) { cancellationToken.ThrowIfCancellationRequested(); // Fast path to try and avoid allocations in callback registration. lock (_syncLock) { if (!this.IsLocked) { this.TakeOwnership(); return; } } cancellationTokenRegistration = cancellationToken.Register(s_cancellationTokenCanceledEventHandler, _syncLock, useSynchronizationContext: false); } using (cancellationTokenRegistration) { // PERF: First spin wait for the lock to become available, but only up to the first planned yield. // This additional amount of spinwaiting was inherited from SemaphoreSlim's implementation where // it showed measurable perf gains in test scenarios. var spin = new SpinWait(); while (this.IsLocked && !spin.NextSpinWillYield) { spin.SpinOnce(); } lock (_syncLock) { while (this.IsLocked) { // If cancelled, we throw. Trying to wait could lead to deadlock. cancellationToken.ThrowIfCancellationRequested(); #if WORKSPACE using (Logger.LogBlock(FunctionId.Misc_NonReentrantLock_BlockingWait, cancellationToken)) #endif { // Another thread holds the lock. Wait until we get awoken either // by some code calling "Release" or by cancellation. Monitor.Wait(_syncLock); } } // We now hold the lock this.TakeOwnership(); } } } /// <summary> /// Exit the mutual exclusion. /// </summary> /// <remarks> /// The calling thread must currently hold the lock. /// </remarks> /// <exception cref="InvalidOperationException">The lock is not currently held by the calling thread.</exception> public void Release() { AssertHasLock(); lock (_syncLock) { this.ReleaseOwnership(); // Release one waiter Monitor.Pulse(_syncLock); } } /// <summary> /// Determine if the lock is currently held by the calling thread. /// </summary> /// <returns>True if the lock is currently held by the calling thread.</returns> public bool LockHeldByMe() => this.IsOwnedByMe; /// <summary> /// Throw an exception if the lock is not held by the calling thread. /// </summary> /// <exception cref="InvalidOperationException">The lock is not currently held by the calling thread.</exception> public void AssertHasLock() => Contract.ThrowIfFalse(LockHeldByMe()); /// <summary> /// Checks if the lock is currently held. /// </summary> private bool IsLocked { get { return _owningThreadId != 0; } } /// <summary> /// Checks if the lock is currently held by the calling thread. /// </summary> private bool IsOwnedByMe { get { return _owningThreadId == Environment.CurrentManagedThreadId; } } /// <summary> /// Take ownership of the lock (by the calling thread). The lock may not already /// be held by any other code. /// </summary> private void TakeOwnership() { Debug.Assert(!this.IsLocked); _owningThreadId = Environment.CurrentManagedThreadId; } /// <summary> /// Release ownership of the lock. The lock must already be held by the calling thread. /// </summary> private void ReleaseOwnership() { Debug.Assert(this.IsOwnedByMe); _owningThreadId = 0; } /// <summary> /// Action object passed to a cancellation token registration. /// </summary> private static readonly Action<object?> s_cancellationTokenCanceledEventHandler = CancellationTokenCanceledEventHandler; /// <summary> /// Callback executed when a cancellation token is canceled during a Wait. /// </summary> /// <param name="obj">The syncLock that protects a <see cref="NonReentrantLock"/> instance.</param> private static void CancellationTokenCanceledEventHandler(object? obj) { RoslynDebug.AssertNotNull(obj); lock (obj) { // Release all waiters to check their cancellation tokens. Monitor.PulseAll(obj); } } public SemaphoreDisposer DisposableWait(CancellationToken cancellationToken = default) { this.Wait(cancellationToken); return new SemaphoreDisposer(this); } /// <summary> /// Since we want to avoid boxing the return from <see cref="NonReentrantLock.DisposableWait"/>, this type must be public. /// </summary> public readonly struct SemaphoreDisposer(NonReentrantLock semaphore) : IDisposable { public void Dispose() => semaphore.Release(); } } |