proposals/csharp-13.0/lock-object.md
Lock object[!INCLUDESpecletdisclaimer]
Champion issue: https://github.com/dotnet/csharplang/issues/7104
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.
.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();
}
}
}
Semantics of the lock statement (§13.13)
are changed to special-case the System.Threading.Lock type:
A
lockstatement of the formlock (x) { ... }
- where
xis an expression of typeSystem.Threading.Lock, is precisely equivalent to:csandusing (x.EnterScope()) { ... }System.Threading.Lockmust have the following shape:csnamespace System.Threading { public sealed class Lock { public Scope EnterScope(); public ref struct Scope { public void Dispose(); } } }- where
xis 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
objectanddynamic.
- A warning is reported when the reference_type is known to be
System.Threading.Lock.- From any class_type
Sto any class_typeT, providedSis derived fromT.
- A warning is reported when
Sis known to beSystem.Threading.Lock.- From any class_type
Sto any interface_typeT, providedSimplementsT.
- A warning is reported when
Sis 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:
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
#pragma warning disable),Monitor APIs directly,object AsObject<T>(T l) => (object)l;.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 when ref structs 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:
lock statement.structs (since the existing lock disallows value types).
There could be problems with default constructors and copying if the structs have lazy initialization.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 Locks 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 in usings with awaits.
I.e., we could emit either an error or a warning for code like using (lockVar.EnterScope()) { await ... }.
Currently, this is not needed since Lock.Scope is a ref struct, so that code is illegal anyway.
However, if we ever allowed ref structs in async methods or changed Lock.Scope to not be a ref 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 with await.)
Alternatively, this could be implemented as an analyzer shipped as part of the runtime.
We could relax the restriction that value types cannot be locked
Lock type (only needed if the API proposal changed it from class to struct),We could allow the new lock in async methods where await is not used inside the lock.
lock is lowered to using with a ref struct as the resource, this results in a compile-time error.
The workaround is to extract the lock into a separate non-async method.ref struct Scope, we could emit Lock.Enter and Lock.Exit methods in try/finally.
However, the Exit method must throw when it's called from a different thread than Enter,
hence it contains a thread lookup which is avoided when using the Scope.using on a ref struct in async methods if there is no await inside the using body.lock patternLock type + adding static analysis warnings