为什么锁在C#中需要实例?
c#
为每个锁对象使用一个对象实例的目的是什么?CLR 是否存储了一个线程在调用Monitor.Enter(instance)时传递的对象的实例,以便当另一个线程尝试输入锁时,CLR 将检查新线程提供的实例,如果该实例与第一个线程实例匹配,则CLR 会将新线程添加到第一个服务队列中,依此类推?
回答
CLR 是否存储了线程在调用 Monitor.Enter(instance) 时传递的对象的实例,以便当另一个线程尝试输入锁时,CLR 将检查新线程提供的实例以及该实例是否匹配到第一个线程实例,然后 CLR 会将新线程添加到第一个服务队列中,依此类推?
不考虑抖动执行的指令重新排序和其他魔术。
首先,让我们解决问题的重要部分:
为什么锁在 C# 中需要实例?
答案并不那么令人满意,但归结为……好吧,必须以某种方式完成!
您可以想象C# 规范和CLR可以使用魔法字符串或数字来跟踪线程同步,但设计人员选择使用引用类型。引用类型已经有一个用于其他 CLR 活动的标头,因此他们没有为您提供保存在表中的幻数或字符串,而是为引用类型选择了双重用途标头以跟踪线程同步。故事基本结束。
更长的故事
Monitor 锁定对象需要是引用类型。值类型没有像引用类型这样的头,部分原因是它们不需要终结并且不能被 GC 固定。此外,值类型可以被装箱,这基本上意味着它们被包装成一个对象。当您将值类型传递给Monitor它们时,它们会被装箱,当您传递相同的 值类型时,它们会被装箱到不同的对象中(这会否定锁的所有内部 CLR 管道)。
这主要是值类型不能用于锁定的原因......
让我们前进
两个值类型和引用类型具有内部存储器的布局。但是,引用类型还包含一个 32 位标头,以帮助 CLR对对象执行某些内务处理任务(如上所述)。这就是我们将要谈论的
头球中有很多内容,但这与火箭科学相去甚远。但是,关于锁定,这里只有 2 个重要的概念,标头锁定状态信息或标头是否需要膨胀到同步块表。
对象头
典型对象头格式中的最高有效字节如下所示。
|31 0|
----------------|
|7|6|5|4|3|2| --|
| | | | | |
| | | | | +- BIT_SBLK_IS_HASHCODE : set if the rest of the word is a hash code (or sync block index)
| | | | +--- BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX : set if hashcode or sync block index is set
| | | +----- BIT_SBLK_SPIN_LOCK : lock the header for exclusive mutation on spin
| | +------- BIT_SBLK_GC_RESERVE : set if the object is pinned
| +--------- BIT_SBLK_FINALIZER_RUN : set if finalized already
+----------- BIT_SBLK_AGILE_IN_PROGRESS : set if locking on AppDomain agile classes
标题是负责持有某些易于获得的信息为CLR,这主要是针对数据的微小位GC,一个hashCode是否已经发生并在锁定状态中的对象。但是,因为对象头(32 位)中只有有限的大小,所以头可能需要膨胀到同步块表。这通常会在以下情况下完成。
- 已生成哈希码并已获取瘦锁。
- 已获得 Fat Lock
- 涉及条件变量(通过等待、脉冲等)
标题不够大。
锁定状态
在对象上创建锁后,CLR将查看标头并首先确定它是否需要在同步块表中查找任何锁定信息,它只需查看设置的位即可完成此操作。如果没有Thin Lock,它将创建一个(如果适用)。如果有Thin Lock,它会尝试旋转并等待它。如果标头已膨胀,它将在同步块表中查找锁定信息(待续...)。
锁定有 2 种不同的风格。临界区和条件变量。
- 关键区域是结果
Enter,Exit,Lock等 - 条件变量是结果
Wait,Pulse等等,这是因为它是不相关的问题,另外一个故事。
关于关键区域,CLR 可以通过两种主要方式为它们锁定。薄锁和胖锁。CLR 在混合锁模型中同时使用这两者,这基本上意味着它首先尝试一个,然后回退到下一个。
薄锁
对象薄锁头
|31 |26 |15 |9 0|
----------------------------------------------------------------
|7|6|5|4|3| App Domain Index | Lock Recusion Level | Thread id |
| | | | |
| | | | |
| | | | +--- BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX = 0 can store a thin lock
一个瘦锁基本上由一个的应用程序域指标,递归级别和管理的主题ID。的主题ID以原子通过锁定螺纹如果设为零,或者如果非零,一个简单的自旋等待状态用来重新读取锁定状态不同的时间获取锁。如果一段时间后锁仍然不可用,它需要提升锁(如果还没有这样做),将薄锁膨胀到同步块表,并且需要向同步块表注册一个真正的* 锁基于内核事件(如自动重置事件)进行操作。
一个瘦锁是它究竟是如何的声音,那是一种重量更轻机制和快速,但是它在纺纱为核心,以实现其工作的成本。这种混合锁定机制对于短发布场景更快且效率更低,但是对于较长的争用场景,CLR 回退到资源密集程度较低的较慢内核锁。简而言之,总的来说,它通常会在日常使用中获得更好的结果。
胖锁
如果发生争用或涉及条件变量(通过 Wait、Pulse 等),则需要在Sync Block 中存储其他信息,例如内核对象的句柄或与锁。胖锁正是它听起来的样子,它是一种更具侵略性的锁,速度较慢,但资源占用较少,因为它不会围绕 CPU 不必要地旋转,它更适合更长的锁周期。
同步块表
对象同步块索引头
|31 |25 0|
--------------------------------
|7|6|5|4|3|2| Sync Block Index |
| | | | | |
| | | | | +- BIT_SBLK_IS_HASHCODE = 0 sync block index
| | | | +--- BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX = 1 hash code or sync block index
CLR在堆上有一个预初始化、可回收、缓存和可重用的同步块表。该表可能包含一个哈希码(从头部迁移),以及对象头部同步块索引(当提升/膨胀发生时)引用的各种类型的锁定信息。
把它们放在一起*
当Monitor.Enter被调用时,CLR 通过将当前线程 ID(除其他外)存储在对象头中(如上所述)或将其提升到Sycnc 块表来注册获取。如果存在Thin Lock,CLR 将通过检查标头或同步块表,短暂地使用自旋来等待锁无争用。
如果自旋锁在经过一定的自旋后无法获得锁,它可能最终需要向操作系统注册一个自动重置事件并将句柄存储在同步块表中。此时,等待线程将只等待该句柄。
那么 CLR 会将新线程添加到第一个服务队列中,依此类推?
不,没有这样的队列,随后这一切都可能导致不公平的行为。线程有能力窃取信号和唤醒之间的锁,但是 CLR 确实以有序的方式帮助实现这一点,并尝试阻止 [锁护送][3]。
因此,显然这里有更多的东西被掩盖了,包括锁的类型(关键区域和条件变量)、CLR 内存模型、回调如何工作等等。但它应该给你一个起点来回答你最初的问题
免责声明:很多信息实际上可能会发生变化,因为它们是 CLR 实现细节。