[Proposal]: Lock statement pattern (VS 17.10, .NET 9)
- Dominant language
- C#
- Stars
- 12.7k
- Forks
- 1.1k
- Avg merge
- 11h 1m
- Merged PRs (30d)
- 3
Description
# Lock statement pattern
* [x] Proposed
* [ ] Prototype: Not Started
* [x] Implementation: https://github.com/dotnet/roslyn/pull/71716
* [x] 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
[motivation]: #motivation
.NET 9 is likely to introduce a new dedicated [System.Threading.Lock type](https://github.com/dotnet/runtime/issues/34812). 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
[design]: #detailed-design
### Example
A type would expose the following to match the proposed pattern:
```C#
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.:
```C#
class MyDataStructure
{
private readonly Lock _lock = new();
void Foo()
{
lock (_lock)
{
// do something
}
}
}
```
would be lowered to the equivalent of:
```C#
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:
- `L` implements interface `ILockPattern`
- `L` has an accessible instance method `S EnterLockScope()` that returns a value of type `S` (`Lock.Scope` in this example). The method must be at least as visible as `L`. Extension methods don't qualify for the pattern.
- A value of type `S` qualifies for use with the `using` keyword
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 `L` does not fully match the lock pattern, it would result in an error
- If a value of type `S` may not be used in the context, it would result in an error
- If a value of type `L` is 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 `this` to a base type when calling an inherited method
- This is intended to prevent accidental usage of `Monitor` with values of type `L` that may be masked under a different type and used with the `lock` keyword, such as with casts to base types, interfaces, etc.
- If a value of type `L` is used with the `lock` keyword, or a value of type `S` is used with the `using` keyword, and the block contains an `await`, 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. `Monitor` and `Lock` have thread affinity.
- `SpinLock` is optionally thread-affinitized. It can opt into the pattern, but then usage of it with the `lock` or `using` keywords would still disallow awaits inside the block statically.
### SpinLock example
`System.Threading.SpinLock` (a struct) could expose such a holder:
```C#
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
[drawbacks]: #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 `L1` that exposes the type is updated to opt into the lock pattern, another library `L2` that references `L1` and was compiled for the previous version of `L1` would have different locking behavior until it is recompiled with the updated version of `L1`.
- The same thing is achievable today with only a little bit more code, using `using` instead of `lock` and 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.
- `ILockPattern` itself doesn't match the lock pattern, though an interface could be created to match the lock pattern.
## Alternatives
[alternatives]: #alternatives
- Use an attribute instead of a marker interface. A marker interface was used because it works more naturally with inheritence. Although `AttributeUsageAttribute.Inherited = true` could 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 with `Monitor`.
- 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 `ref` keyword 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
[unresolved]: #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
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.