注意
本文是特性规范。 该规范充当该功能的设计文档。 它包括建议的规范更改,以及功能设计和开发过程中所需的信息。 这些文章将发布,直到建议的规范更改最终确定并合并到当前的 ECMA 规范中。
功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的语言设计会议 (LDM) 说明中。
可以在 规范一文中详细了解将功能规范采用 C# 语言标准的过程。
支持者问题:https://github.com/dotnet/csharplang/issues/7104
总结
System.Threading.Lock
如何与 lock
关键字交互的特殊情况(在后台调用其 EnterScope
方法)。
添加静态分析警告,以防止尽可能意外滥用该类型。
动机
.NET 9 引入了新的 System.Threading.Lock
类型,作为现有基于监视器的锁定的更好替代方法。
C# 中存在 lock
关键字可能会导致开发人员认为他们可以将此关键字用于此新类型。
这样做不会根据该类型特定定义的语义进行锁定,而是将其视为普通对象,并使用监视器锁定机制。
namespace System.Threading
{
public sealed class Lock
{
public void Enter();
public void Exit();
public Scope EnterScope();
public ref struct Scope
{
public void Dispose();
}
}
}
详细设计
lock 语句 (§13.13) 的语义被更改为特殊情况下的 System.Threading.Lock
类型:
形式为
lock (x) { ... }
的lock
语句
- 其中
x
是System.Threading.Lock
类型的表达式,精确等同于:和using (x.EnterScope()) { ... }
System.Threading.Lock
必须具有以下形状:namespace System.Threading { public sealed class Lock { public Scope EnterScope(); public ref struct Scope { public void Dispose(); } } }
- 其中,
x
是 reference_type的表达式,其精确等效于: ~
请注意,如果 Lock
类型未 sealed
,则形状可能不会完全检查(例如,如果 Lock
类型未 sealed
,则不会显示任何错误和警告),但该功能可能无法按预期工作(例如,将 Lock
转换为派生类型时不会有警告,因为该功能假定没有派生类型)。
此外,在向上转换 System.Threading.Lock
类型时,隐式引用转换 (§10.2.8) 中添加了新的警告:
隐式引用转换包括:
- 从任何 reference_type 到
object
和dynamic
。
- 当 reference_type 已知为
System.Threading.Lock
时报告警告。- 从任意 class_type
S
到任意 class_typeT
,条件是S
派生自T
。
- 当
S
已知为System.Threading.Lock
时报告警告。- 从任何 class_type
S
转换为任何 interface_typeT
,条件是S
已实现T
。
- 当
S
已知为System.Threading.Lock
时报告警告。- [...]
object l = new System.Threading.Lock(); // warning
lock (l) { } // monitor-based locking is used here
请注意,即使在等效的显式转换中也会发生此警告。
编译器在某些情况下,避免在转换为 object
后实例无法被锁定时报告警告:
- 当转换为隐式转换并且是对象相等性运算符调用的一部分时。
var l = new System.Threading.Lock();
if (l != null) // no warning even though `l` is implicitly converted to `object` for `operator!=(object, object)`
// ...
若要摆脱警告并强制使用基于监视器的锁定,可以使用
- 通常的警告抑制方法 (
#pragma warning disable
), - 直接使用
Monitor
API, - 间接强制转换,如
object AsObject<T>(T l) => (object)l;
。
替代方案
支持一种通用模式,其他类型也可以使用该模式与
lock
关键字进行交互。 这是一项未来的工作,可能会在ref struct
可以参与泛型时实现。 LDM 2023-12-04 中进行了讨论。为了避免现有基于监视器的锁定和新
Lock
(或将来的模式)之间存在歧义,我们可以:- 引入新语法,而不是重用现有
lock
语句。 - 要求使用新的锁类型
struct
s,因为现有的lock
不允许使用值类型。 如果结构具有延迟初始化,则默认构造函数和复制可能存在问题。
- 引入新语法,而不是重用现有
可以加强代码生成器,以抵御线程中止(它们本身已过时)。
当
Lock
作为类型参数传递时,我们还可能会发出警告,因为对类型参数的锁定始终使用基于监视器的锁定:M(new Lock()); // could warn here void M<T>(T x) // (specifying `where T : Lock` makes no difference) { lock (x) { } // because this uses Monitor }
但是,这会导致在将
Lock
存储到列表中时发出警告,这是不可取的。List<Lock> list = new(); list.Add(new Lock()); // would warn here
我们可以包括静态分析,以防止在包含
await
的using
中使用System.Threading.Lock
。 例如,我们可以针对using (lockVar.EnterScope()) { await ... }
等代码发出错误或警告。 目前不需要这样做,因为Lock.Scope
是ref struct
,因此代码无论如何都是非法的。 但是,如果我们允许ref struct
出现在async
方法中或将Lock.Scope
更改为不再是ref struct
,那么这种分析将会变得有益。 (如果将来实现,我们可能需要考虑所有符合常规模式的锁类型。尽管可能需要一个选择退出机制,因为某些锁类型可能被允许与await
一起使用。)或者,这可以作为运行时附带的分析器来实现。我们可以放宽值类型不能
lock
的限制- 对于新的
Lock
类型(只有当 API 提议将其从class
更改为struct
时才需要。) - 对于未来实现时任何类型都可以参与的通用模式。
- 对于新的
我们可以在
async
方法中允许新的lock
,其中await
不在lock
中使用。- 目前,由于
lock
已降低到using
,ref struct
作为资源,因此会导致编译时错误。 解决方法是将lock
提取到单独的非async
方法中。 - 我们可以在
try
/finally
中发出Lock.Enter
和Lock.Exit
方法,而不是使用ref struct Scope
。 然而,当从与Enter
不同的线程调用Exit
方法时,它必须抛出,因此它包含一个线程查找,而使用Scope
时可以避免。 - 如果
using
正文中没有await
,最好允许在async
方法中的ref struct
上编译using
。
- 目前,由于
设计会议
- LDM 2023-05-01:支持
lock
模式的初始决策 - LDM 2023-10-16:分类到 .NET 9 的工作集
- LDM 2023-12-04:拒绝了常规模式,仅接受
Lock
类型的特殊情况处理 + 添加静态分析警告。