Description
openedon Apr 10, 2020
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 aref struct
. Aref struct
helps to make the exit code path less expensive and can improve performance in some cases, as it guarantees thatScope.Dispose
is called on the same thread asEnterScope
, and the path that exits the lock would be able to avoid an extra thread-static lookup for the current thread ID. Similarly forTryScope
.- For the
TryEnterScope
methods, an alternative is to return a nullable struct. It seems more readable to check forWasEntered
than to check for null or theHasValue
property to see if the lock was entered. Also aref struct
can't be nullable currently, and a regular struct would not benefit from a potentially less expensive exit path. IsHeld
instead ofIsHeldByCurrentThread
. The former is simpler, though may be a bit ambiguous, and may be confusing againstSpinLock.IsHeld
, where it meansIsHeldByAnyThread
.- 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
- There may be some changes based on the language proposal in [Proposal]: Lock statement pattern (VS 17.10, .NET 9) csharplang#7104