Skip to content

Add first class System.Threading.Lock type #34812

Closed

Description

Background and Motivation

Locking on any class has overhead from the dual role of the syncblock as both lock field and hashcode et al. (e.g. #34800)

Adding a first class lock type that didn't allow alternative uses and only acted as a lock would allow for a simpler and faster lock as well as be less ambiguous on type and purpose in source code.

API proposal

[Edit] by @kouvel based on #34812 (comment) and dotnet/csharplang#7104

namespace System.Threading
{
    // This is a lock that can be entered by one thread at a time. A thread may enter the lock recursively multiple
    // times, in which case the thread should also exit the lock the same number of times to fully exit the lock.
    public sealed class Lock
    {
        // Exceptions:
        //   - LockRecursionException if the lock was entered recursively by the current thread the maximum number of times
        public void Enter();

        // Returns true if the lock was acquired by the current thread, false otherwise.
        // Exceptions:
        //   - LockRecursionException if the lock was entered recursively by the current thread the maximum number of times.
        //     In this corner case an exception would be better than returning false, as the calling thread cannot make
        //     further progress with entering the lock before exiting it.
        public bool TryEnter();

        // Returns true if the lock was acquired by the current thread, false otherwise.
        // Exceptions:
        //   - ArgumentOutOfRangeException if the timeout value converted to an integer milliseconds value is negative
        //     and not equal to -1, or greater than Int32.MaxValue
        //   - LockRecursionException if the lock was entered recursively by the current thread the maximum number of times
        //     In this corner case an exception would be better than returning false, as the calling thread cannot make
        //     further progress with entering the lock before exiting it.
        public bool TryEnter(TimeSpan timeout);

        // Returns true if the lock was acquired, false otherwise.
        // Exceptions:
        //   - ArgumentOutOfRangeException if the timeout value is negative and not equal to -1
        //   - LockRecursionException if the lock was entered recursively by the current thread the maximum number of times
        //     In this corner case an exception would be better than returning false, as the calling thread cannot make
        //     further progress with entering the lock before exiting it.
        public bool TryEnter(int millisecondsTimeout);

        // Exceptions: SynchronizationLockException if the current thread does not own the lock
        public void Exit();

        // Returns true if the current thread holds the lock, false otherwise.
        //
        // This is analogous to Monitor.IsEntered(), but more similar to SpinLock.IsHeldByCurrentThread. There could
        // conceivably be a Lock.IsHeld as well similarly to SpinLock.IsHeld, or Lock.IsHeldByAnyThread to be more
        // precise, which would return true if the lock is held by any thread.
        public bool IsHeldByCurrentThread { get; }

        // Enters the lock and returns a holder. Enables integration with the "lock" keyword.
        // Exceptions:
        //   - LockRecursionException if the lock was entered recursively by the current thread the maximum number of times
        public Scope EnterScope();

        public ref struct Scope
        {
            // Exits the lock. No-op if Dispose was already called.
            // Exceptions: SynchronizationLockException if the current thread does not own the lock
            public void Dispose();
        }
    }
}

API Usage

Change in usage

private object _lock = new object();

Becomes the clearer

private Lock _lock = new Lock();

[Edit] by @kouvel

Usage example 1

_lock.Enter();
try
{
    // do something
}
finally
{
    _lock.Exit();
}

Usage example 2

using (_lock.EnterScope())
{
    // do something
}

Usage example 3

After the lock statement integration described in #34812 (comment), the following would use Lock.EnterScope and Scope.Dispose instead of using Monitor.

lock (_lock)
{
    // do something
}

TryEnterScope usage example

The following:

if (_lock.TryEnter())
{
    try
    {
        // do something
    }
    finally { _lock.Exit(); }
}

Could be slightly simplified to:

using (var tryScope = _lock.TryEnterScope())
{
    if (tryScope.WasEntered)
    {
        // do something
    }
}

The latter could also help to make the exit code path less expensive by avoiding a second thread-static lookup for the current thread ID.

Alternative Designs

  • Scope could be a regular struct instead of a ref struct. A ref struct helps to make the exit code path less expensive and can improve performance in some cases, as it guarantees that Scope.Dispose is called on the same thread as EnterScope, and the path that exits the lock would be able to avoid an extra thread-static lookup for the current thread ID. Similarly for TryScope.
  • For the TryEnterScope methods, an alternative is to return a nullable struct. It seems more readable to check for WasEntered than to check for null or the HasValue property to see if the lock was entered. Also a ref struct can't be nullable currently, and a regular struct would not benefit from a potentially less expensive exit path.
  • IsHeld instead of IsHeldByCurrentThread. The former is simpler, though may be a bit ambiguous, and may be confusing against SpinLock.IsHeld, where it means IsHeldByAnyThread.
  • Making the Lock type a struct instead. This may save some allocations in favor of increasing the size of the containing type. Structs can be a bit more cumbersome to work with, and the benefits may not be large enough to be worthwhile.

Risks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions