Lock 对象

注意

本文是特性规范。 该规范充当该功能的设计文档。 它包括建议的规范更改,以及功能设计和开发过程中所需的信息。 这些文章将发布,直到建议的规范更改最终确定并合并到当前的 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 语句

  1. 其中 xSystem.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();
            }
        }
    }
    
  2. 其中,xreference_type的表达式,其精确等效于: ~

请注意,如果 Lock 类型未 sealed,则形状可能不会完全检查(例如,如果 Lock 类型未 sealed,则不会显示任何错误和警告),但该功能可能无法按预期工作(例如,将 Lock 转换为派生类型时不会有警告,因为该功能假定没有派生类型)。

此外,在向上转换 System.Threading.Lock 类型时,隐式引用转换 (§10.2.8) 中添加了新的警告:

隐式引用转换包括:

  • 从任何 reference_typeobjectdynamic
    • reference_type 已知为 System.Threading.Lock 时报告警告。
  • 从任意 class_typeS 到任意 class_typeT,条件是 S 派生自 T
    • S 已知为 System.Threading.Lock 时报告警告。
  • 从任何 class_typeS 转换为任何 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 语句。
    • 要求使用新的锁类型 structs,因为现有的 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
    
  • 我们可以包括静态分析,以防止在包含 awaitusing 中使用 System.Threading.Lock。 例如,我们可以针对 using (lockVar.EnterScope()) { await ... }等代码发出错误或警告。 目前不需要这样做,因为 Lock.Scoperef struct,因此代码无论如何都是非法的。 但是,如果我们允许 ref struct 出现在 async 方法中或将 Lock.Scope 更改为不再是 ref struct,那么这种分析将会变得有益。 (如果将来实现,我们可能需要考虑所有符合常规模式的锁类型。尽管可能需要一个选择退出机制,因为某些锁类型可能被允许与 await 一起使用。)或者,这可以作为运行时附带的分析器来实现。

  • 我们可以放宽值类型不能 lock 的限制

    • 对于新的 Lock 类型(只有当 API 提议将其从 class 更改为 struct 时才需要。)
    • 对于未来实现时任何类型都可以参与的通用模式。
  • 我们可以在 async 方法中允许新的 lock,其中 await 不在 lock 中使用。

    • 目前,由于 lock 已降低到 usingref struct 作为资源,因此会导致编译时错误。 解决方法是将 lock 提取到单独的非 async 方法中。
    • 我们可以在 try/finally中发出 Lock.EnterLock.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 类型的特殊情况处理 + 添加静态分析警告。