File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\Utilities\NonReentrantLock.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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;
 
#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();
    }
}