Lock
object
Note
This article is a feature specification. The specification serves as the design document for the feature. It includes proposed specification changes, along with information needed during the design and development of the feature. These articles are published until the proposed spec changes are finalized and incorporated in the current ECMA specification.
There may be some discrepancies between the feature specification and the completed implementation. Those differences are captured in the pertinent language design meeting (LDM) notes.
You can learn more about the process for adopting feature speclets into the C# language standard in the article on the specifications.
Summary
Special-case how System.Threading.Lock
interacts with the lock
keyword (calling its EnterScope
method under the hood).
Add static analysis warnings to prevent accidental misuse of the type where possible.
Motivation
.NET 9 is introducing a new System.Threading.Lock
type
as a better alternative to existing monitor-based locking.
The presence of the lock
keyword in C# might lead developers to think they can use it with this new type.
Doing so wouldn't lock according to the semantics of this type but would instead treat it as any other object and would use monitor-based locking.
namespace System.Threading
{
public sealed class Lock
{
public void Enter();
public void Exit();
public Scope EnterScope();
public ref struct Scope
{
public void Dispose();
}
}
}
Detailed design
Semantics of the lock statement (§13.13)
are changed to special-case the System.Threading.Lock
type:
A
lock
statement of the formlock (x) { ... }
- where
x
is an expression of typeSystem.Threading.Lock
, is precisely equivalent to:andusing (x.EnterScope()) { ... }
System.Threading.Lock
must have the following shape:namespace System.Threading { public sealed class Lock { public Scope EnterScope(); public ref struct Scope { public void Dispose(); } } }
- where
x
is an expression of a reference_type, is precisely equivalent to: [...]
Note that the shape might not be fully checked (e.g., there will be no errors nor warnings if the Lock
type is not sealed
),
but the feature might not work as expected (e.g., there will be no warnings when converting Lock
to a derived type,
since the feature assumes there are no derived types).
Additionally, new warnings are added to implicit reference conversions (§10.2.8)
when upcasting the System.Threading.Lock
type:
The implicit reference conversions are:
- From any reference_type to
object
anddynamic
.
- A warning is reported when the reference_type is known to be
System.Threading.Lock
.- From any class_type
S
to any class_typeT
, providedS
is derived fromT
.
- A warning is reported when
S
is known to beSystem.Threading.Lock
.- From any class_type
S
to any interface_typeT
, providedS
implementsT
.
- A warning is reported when
S
is known to beSystem.Threading.Lock
.- [...]
object l = new System.Threading.Lock(); // warning
lock (l) { } // monitor-based locking is used here
Note that this warning occurs even for equivalent explicit conversions.
The compiler avoids reporting the warning in some cases when the instance cannot be locked after converting to object
:
- when the conversion is implicit and part of an object equality operator invocation.
var l = new System.Threading.Lock();
if (l != null) // no warning even though `l` is implicitly converted to `object` for `operator!=(object, object)`
// ...
To escape out of the warning and force use of monitor-based locking, one can use
- the usual warning suppression means (
#pragma warning disable
), Monitor
APIs directly,- indirect casting like
object AsObject<T>(T l) => (object)l;
.
Alternatives
Support a general pattern that other types can also use to interact with the
lock
keyword. This is a future work that might be implemented whenref struct
s can participate in generics. Discussed in LDM 2023-12-04.To avoid ambiguity between the existing monitor-based locking and the new
Lock
(or pattern in the future), we could:- Introduce a new syntax instead of reusing the existing
lock
statement. - Require the new lock types to be
struct
s (since the existinglock
disallows value types). There could be problems with default constructors and copying if the structs have lazy initialization.
- Introduce a new syntax instead of reusing the existing
The codegen could be hardened against thread aborts (which are themselves obsoleted).
We could warn also when
Lock
is passed as a type parameter, because locking on a type parameter always uses monitor-based locking:M(new Lock()); // could warn here void M<T>(T x) // (specifying `where T : Lock` makes no difference) { lock (x) { } // because this uses Monitor }
However, that would cause warnings when storing
Lock
s in a list which is undesirable:List<Lock> list = new(); list.Add(new Lock()); // would warn here
We could include static analysis to prevent usage of
System.Threading.Lock
inusing
s withawait
s. I.e., we could emit either an error or a warning for code likeusing (lockVar.EnterScope()) { await ... }
. Currently, this is not needed sinceLock.Scope
is aref struct
, so that code is illegal anyway. However, if we ever allowedref struct
s inasync
methods or changedLock.Scope
to not be aref struct
, this analysis would become beneficial. (We would also likely need to consider for this all lock types matching the general pattern if implemented in the future. Although there might need to be an opt-out mechanism as some lock types might be allowed to be used withawait
.) Alternatively, this could be implemented as an analyzer shipped as part of the runtime.We could relax the restriction that value types cannot be
lock
ed- for the new
Lock
type (only needed if the API proposal changed it fromclass
tostruct
), - for the general pattern where any type can participate when implemented in the future.
- for the new
We could allow the new
lock
inasync
methods whereawait
is not used inside thelock
.- Currently, since
lock
is lowered tousing
with aref struct
as the resource, this results in a compile-time error. The workaround is to extract thelock
into a separate non-async
method. - Instead of using the
ref struct Scope
, we could emitLock.Enter
andLock.Exit
methods intry
/finally
. However, theExit
method must throw when it's called from a different thread thanEnter
, hence it contains a thread lookup which is avoided when using theScope
. - Best would be to allow compiling
using
on aref struct
inasync
methods if there is noawait
inside theusing
body.
- Currently, since
Design meetings
- LDM 2023-05-01: initial decision to support a
lock
pattern - LDM 2023-10-16: triaged into the working set for .NET 9
- LDM 2023-12-04: rejected the general pattern, accepted only special-casing the
Lock
type + adding static analysis warnings
C# feature specifications