CLR 是否存储了由线程传递的对象的实例
调用 Monitor.Enter(instance) 以便当另一个线程尝试
输入一个锁,CLR 将检查新提供的实例
线程,如果实例与第一个线程实例匹配,则
CLR 会将新线程添加到先服务队列中,并且
等等?
忽略指令重新排序和抖动执行的其他魔法。
首先,让我们解决问题的重要部分:
为什么锁需要 C# 中的实例?
答案并不那么令人满意,但归结为……嗯,必须以某种方式完成!
您可以想象 C# 规范 和 CLR 可以使用 魔术字符串 或 数字为了跟踪线程同步,设计人员选择使用 Reference Types。 引用类型已经有一个 header 用于其他 CLR 活动,所以不要给你 magic numbers 或 strings保留在一个表中,他们为 reference 类型选择了一个 双重用途 标头来跟踪 线程同步。故事基本结束。
更长的故事
Monitor 锁定对象需要是引用类型。 Value Types 没有像 Reference Types 这样的标头,部分原因是它们不需要最终确定并且不能被 GC 固定。此外,值类型可以被装箱,这基本上意味着它们被包装到一个对象中。当您将值类型传递给Monitor时,它们会被装箱,当您传递相同 值类型时,它们会被装箱到不同的对象中(这否定了 lock 的所有内部 CLR 管道)。
这主要是值类型不能用于锁定的原因...
让我们继续前进
值类型和引用类型都有一个内部内存布局。但是,reference types 还包含一个 32 位 header 以帮助 CLR 在 object 上执行某些 housekeeping 任务(如上所述)。这就是我们要讨论的内容
头球中有很多内容,但这与火箭科学相去甚远。虽然,关于锁定,这里只有 2 个概念很重要,标头 Lock State 信息或标头是否需要膨胀到 Sync Block Table。
对象头
典型对象头格式中最重要的字节如下所示。
|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
header 负责为 CLR 保存某些易于访问的信息,主要是 GC 的一小部分数据,是否已生成 HashCode 以及 Lock State 对象的em>。不过,由于对象标头(32 位)中只有有限的大小,因此标头可能需要扩充到同步块表。这通常会在以下情况下完成。
- 已生成哈希码并已获取瘦锁。
- 已获得 Fat Lock
- 涉及条件变量(通过等待、脉冲等)
标题不够大。
锁定状态
一旦你在一个object上创建了一个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
Thin Lock 基本上由 App Domain Index、Recursion Level 和 Managed Thread Id 组成. Thread Id 由锁定线程自动设置,如果为零,或者如果非零,则使用简单的自旋等待多次重新读取锁定状态以获取锁定。如果一段时间后锁仍然不可用,则需要提升锁(如果尚未这样做),将 Thin Lock 膨胀到 Sync Block Table em> 和 true* 锁需要注册到基于内核事件的操作(如自动重置事件)。
Thin Lock 正是它听起来的样子,它是一种重量更轻且速度更快的机构,但它是以旋转核心为代价来实现其工作的。这种混合锁定机制对于短期发布场景来说更快且效率更低,但是对于较长的争用场景,CLR 会退回到资源密集度较低的较慢内核锁。简而言之,总体而言,它通常会在日常使用中获得更好的结果。
脂肪锁
如果发生争用或涉及条件变量(通过等待、脉冲等),则需要在同步块中存储其他信息,例如作为内核对象的句柄或与锁关联的事件列表。胖锁就是它听起来的样子,它是一种更具侵略性的锁,速度较慢,但资源密集度较低,因为它不会围绕 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 实现细节。