c# - 为什么锁需要 C# 中的实例?
问题描述
为每个锁对象使用一个对象实例的目的是什么?CLR 是否存储了一个线程在调用时传递的对象的实例,Monitor.Enter(instance)
以便当另一个线程尝试进入锁时,CLR 将检查新线程提供的实例,如果该实例与第一个线程实例匹配,那么CLR 会将新线程添加到先服务队列中,等等?
解决方案
CLR 是否存储在调用 Monitor.Enter(instance) 时由线程传递的对象实例,以便当另一个线程尝试进入锁时,CLR 将检查新线程提供的实例以及实例是否匹配到第一个线程实例,然后 CLR 会将新线程添加到第一个服务队列中的第一个,等等?
忽略由抖动执行的指令重新排序和其他魔术。
首先,让我们解决问题的重要部分:
为什么锁需要 C# 中的实例?
答案并不那么令人满意,但归结为……嗯,它必须以某种方式完成!
您可以想象C# 规范和CLR可以使用魔术字符串或数字来跟踪线程同步,但设计人员选择使用引用类型。引用类型已经有一个用于其他 CLR 活动的标头,因此他们没有为您提供保留在表中的幻数或字符串,而是选择了一个双重用途标头作为引用类型来跟踪线程同步。故事基本结束。
更长的故事
Monitor
锁定对象需要是引用类型。值类型没有像Reference Types这样的标头,部分原因是它们不需要终结并且不能被 GC 固定。此外,值类型可以被装箱,这基本上意味着它们被包装到一个对象中。当您将值类型传递给Monitor
它们时,它们会被装箱,当您传递相同的 值类型时,它们会被装箱到不同的对象中(这否定了lock的所有内部 CLR 管道)。
这主要是为什么值类型不能用于锁定的原因......
让我们继续前进
值类型和引用类型都有内部存储器布局。但是,引用类型还包含一个 32 位标头,以帮助 CLR对对象执行某些内务处理任务(如上所述)。这就是我们将要谈论的
标题中有相当多的内容,但它与火箭科学相去甚远。虽然,关于锁定,这里只有两个概念很重要,标头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
标头负责为 CLR 保存某些易于访问的信息,主要是用于GC的微小数据,是否已生成 HashCode 以及对象的锁定状态。但是,由于对象标头(32 位)中只有有限的大小,因此标头可能需要膨胀到Sync Block Table。这通常会在以下情况下完成。
- 已生成哈希码并已获取瘦锁。
- 已获得 Fat Lock
- 涉及条件变量(通过等待、脉冲等)
标题不够大。
锁定状态
在对象上创建锁后,CLR将查看标头并首先确定是否需要在同步块表中查找任何锁定信息,它只需查看设置的位即可。如果没有Thin Lock,它将创建一个(如果适用)。如果有薄锁,它将尝试旋转并等待它。如果标头已膨胀,它将在同步块表中查找锁定信息(待续...)。
Locking 有 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组成。线程 ID由锁定线程原子设置,如果为零,或者如果非零,则使用简单的自旋等待多次重新读取锁定状态以获取锁定。如果一段时间后锁仍然不可用,则需要提升锁(如果尚未这样做),将Thin Lock膨胀到同步块表,并且需要向同步块表注册一个真正的* 锁基于内核事件(如自动重置事件)进行操作。
Thin Lock正是它听起来的样子,它是一种重量更轻、速度更快的机制,但它的代价是旋转核心来完成它的工作。这种混合锁定机制在短期发布场景中更快且效率更低,但是对于较长的争用场景,CLR 会退回到资源密集度较低的较慢内核锁。简而言之,总体而言,它通常会在日常使用中获得更好的结果。
脂肪锁
在发生争用或涉及条件变量的情况下(通过等待、脉冲等),需要将其他信息存储在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 Block Table来注册获取。如果存在Thin Lock ,CLR 将通过检查标头或Sync Block Table短暂地使用自旋来等待锁被取消竞争。
如果自旋锁在自旋一定次数后无法获得锁,最终可能需要向操作系统注册一个自动重置事件,并将句柄存储在Sync Block Table中。此时,等待线程将只等待该句柄。
那么CLR会将新线程添加到先服务队列中,等等?
不,没有这样的队列,随后这一切都可能导致不公平的行为。线程有能力窃取信号和唤醒之间的锁,但是 CLR 确实以有序的方式帮助了这一点,并试图阻止 [锁护卫队][3]。
因此,这里显然有很多关于锁的类型(关键区域和条件变量)、CLR 内存模型、回调如何工作等内容被掩盖了。但它应该给你一个起点来回答你最初的问题
免责声明:许多此类信息实际上可能会发生变化,因为它们是 CLR 实现细节。
推荐阅读
- http - 如何在不编码的情况下将冒号嵌入 URL 的片段部分?
- c++ - 根据用户输入生成年、月、周和日
- c# - C# for 循环语法
- git - 是否存在加法合并策略?
- docker - 当文件名相同时,Docker 将 COPY 的第一个文件被 COPY 的第二个文件覆盖
- curl - pushover + curl --form-string: 使用不当
- python - 根据另一个数据集过滤数据集
- javascript - 将对象数组作为参数传递给rails控制器
- apache - 将本地 Apache2 服务器的“IP 地址”部分更改为某个一致的字符串
- javascript - HTML5 Canvas Context.drawImage 未显示