-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Description
Lock statement pattern
- Proposed
- Prototype: Not Started
- Implementation: Add
Lockobject feature roslyn#71716 - Specification: https://github.com/dotnet/csharplang/blob/main/proposals/lock-object.md
(This proposal comes from @kouvel. I've populated this issue primarily with text he wrote in a separate document and augmented it with a few more details.)
Summary
Enable types to define custom behaviors for entering and exiting a lock when an instance of the type is used with the C# “lock” keyword.
Motivation
.NET 9 is likely to introduce a new dedicated System.Threading.Lock type. Along with other custom locks, the presence of the lock keyword in C# might lead developers to think they can use it in conjunction with this new type, but doing so won't actually lock according to the semantics of the lock type and would instead treat it as any arbitrary object for use with Monitor.
Detailed design
Example
A type would expose the following to match the proposed pattern:
class Lock : ILockPattern
{
public Scope EnterLockScope();
public ref struct Scope
{
public void Dispose();
}
}
public interface ILockPattern { }EnterLockScope() would enter the lock and Dispose() would exit the lock. The behaviors of entering and exiting the lock are defined by the type.
The ILockPattern interface is a marker interface that indicates that usage of values of this type with the lock keyword would override the normal code generation for arbitrary objects. Instead, the compiler would lower the lock to use the lock pattern, e.g.:
class MyDataStructure
{
private readonly Lock _lock = new();
void Foo()
{
lock (_lock)
{
// do something
}
}
}would be lowered to the equivalent of:
class MyDataStructure
{
private readonly Lock _lock = new();
void Foo()
{
using (_lock.EnterLockScope())
{
// do something
}
}
}Lock pattern and behavior details
Consider a type L (Lock in this example) that may be used with the lock keyword. If L matches the lock pattern, it would meet all of the following criteria:
Limplements interfaceILockPatternLhas an accessible instance methodS EnterLockScope()that returns a value of typeS(Lock.Scopein this example). The method must be at least as visible asL. Extension methods don't qualify for the pattern.- A value of type
Squalifies for use with theusingkeyword
A marker interface ILockPattern is used to opt into the behaviors below, including through inheritance, and so that S may be defined by the user (for instance, as a ref struct). For a type L that implements interface ILockPattern:
- If
Ldoes not fully match the lock pattern, it would result in an error - If a value of type
Smay not be used in the context, it would result in an error - If a value of type
Lis implicitly or explicitly casted to another type that does not match the lock pattern (including generic types), it would result in a warning- This includes implicit casts of
thisto a base type when calling an inherited method - This is intended to prevent accidental usage of
Monitorwith values of typeLthat may be masked under a different type and used with thelockkeyword, such as with casts to base types, interfaces, etc.
- This includes implicit casts of
- If a value of type
Lis used with thelockkeyword, or a value of typeSis used with theusingkeyword, and the block contains anawait, it would result in an error- The warning would only be issued if in-method analysis can detect it
- This is intended to prevent the enter and exit from occurring on different threads.
MonitorandLockhave thread affinity. SpinLockis optionally thread-affinitized. It can opt into the pattern, but then usage of it with thelockorusingkeywords would still disallow awaits inside the block statically.
SpinLock example
System.Threading.SpinLock (a struct) could expose such a holder:
struct SpinLock : ILockPattern
{
[UnscopedRef]
public Scope EnterLockScope();
public ref struct Scope
{
public void Dispose();
}
}and then similarly be usable with lock whereas today as a struct it's not. When a variable of a struct type that matches the lock pattern is used with the lock keyword, it would be used by reference. Note the reference to SpinLock in the previous section.
Drawbacks
- If an existing reference type opted-in to using the pattern, it would change the meaning of
lock(objectsOfThatType)in existing code, and in ways that might go unnoticed due to the general unobservability of locks (when things are working). This also means if System.Threading.Lock is introduced before the language support, it likely couldn't later participate.- Also if one library
L1that exposes the type is updated to opt into the lock pattern, another libraryL2that referencesL1and was compiled for the previous version ofL1would have different locking behavior until it is recompiled with the updated version ofL1.
- Also if one library
- The same thing is achievable today with only a little bit more code, using
usinginstead oflockand writing out the name of the enter method manually (i.e. writing out the lowered code in the first example). - Having the lock semantics vary based on the static type of the variable is brittle.
ILockPatternitself doesn't match the lock pattern, though an interface could be created to match the lock pattern.
Alternatives
- Use an attribute instead of a marker interface. A marker interface was used because it works more naturally with inheritence. Although
AttributeUsageAttribute.Inherited = truecould be used for an attribute, it doesn't seem to work with interfaces. - Use a new syntax like
using lock(x)or other alternatives instead, which would eliminate any possibility of misusing values of the type withMonitor. - System.Threading.Lock could be special-cased by the compiler rather than having it be a general pattern.
- Various naming options in the pattern itself.
- Instead of having a holder type, instead have the compiler explicitly recognize certain enter/exit methods by naming convention or attribution.
- Potentially allow/require the
refkeyword for certain uses, e.g. in the SpinLock example. - The pattern as suggested doesn't support being hardened against thread aborts (which are themselves obsoleted). It could be extended to now or in the future.
Unresolved questions
Design meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-01.md#lock-statement-improvements
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-16.md#lock-statement-pattern
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-12-04.md#lock-statement-pattern