为什么在 C# 中锁需要实例?
Why Do Locks Require Instances In C#?
为每个锁对象使用对象实例的目的是什么? CLR 是否存储了一个线程在调用 Monitor.Enter(instance)
时传递的对象实例,以便当另一个线程试图进入锁时,CLR 将检查新线程提供的实例以及该实例是否匹配第一个线程实例,然后 CLR 会将新线程添加到先到先服务队列中,依此类推?
Is the CLR storing an instance of an object passed by a thread on a
call to Monitor.Enter(instance) so that when another thread tries to
enter a lock, the CLR will check the instance provided by the new
thread and if the instance matches to the first threads instance then
the CLR will add the new thread to a first in first served queue and
so on?
不考虑抖动执行的指令重新排序和其他魔法。
首先,让我们解决问题的重要部分:
Why Do Locks Require Instances In C#?
答案不是那么令人满意,但归结为......好吧,它必须以某种方式完成!
您可以想象 C# 规范 和 CLR 可以使用 魔术字符串 , 或 number 来跟踪线程同步,但设计者选择使用 Reference Types。 引用类型 已经有一个 header 用于其他 CLR activity,所以不用给你 magic numbers 或 strings 保存在 table 中,他们选择了 双重用途 header用于 引用类型 以跟踪 线程同步 。故事基本结束。
更长的故事
Monitor
锁objects需要引用类型。 值类型没有像引用类型那样的header,部分原因是它们不需要完成并且不能被固定GC。此外,值类型可以盒装,这基本上意味着它们被包装成object.当您将 值类型 传递给 Monitor
时,它们会得到 盒装 ,当您将 相同 value type 它们被装箱到不同的 object(这否定了 lock 的所有内部 CLR 管道)。
这就是为什么值类型不能用于锁定的主要原因...
我们继续前进
值类型和引用类型都有内部内存布局。但是,引用类型 还包含 32 位 header 以帮助 CLR 执行某些 内务处理 object 上的任务(如上所述)。这就是我们要说的
header 中发生了一些事情,但它远非火箭科学。虽然,关于锁定,这里只有 2 个概念很重要,header Lock State 信息或 header 是否需要膨胀到 同步块Table.
Object header
典型 object header 格式中的最高有效字节如下所示。
|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以及object的锁定状态。但是,因为 object header(32 位)中的大小有限,所以 header 可能需要 膨胀 到 同步块 Table。这通常会在以下情况下完成。
- 已生成哈希码并已获取精简锁。
- 获得发锁
- 涉及条件变量(通过 Wait、Pulse 等)
header 不够大。
锁定状态
在 object 上创建 lock 后,CLR将查看 header 并首先确定它是否需要在 同步块 table 中找到任何锁定信息,它通过查看被设置。如果没有 Thin Lock,它将创建一个(如果适用)。如果有 Thin Lock 它会尝试旋转并等待它。如果 header 已膨胀,它将在同步块 Table 中查找锁定信息(待续...)。
锁定有两种不同的风格。 关键区域和条件变量。
- 临界区是
Enter
、Exit
、Lock
等 的结果
- 条件变量是
Wait
、Pulse
等的结果,这是另外一个故事,因为它与问题无关。
关于临界区,CLR 可以通过两种主要方式为其锁定。 瘦锁,以及胖锁。 CLR 在混合锁模型中使用这两者,这基本上意味着它先尝试一个然后回退到下一个。
薄锁
Object 薄锁 Header
|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
A Thin Lock 基本上由 App Domain Index、Recursion Level 和托管线程 ID。 Thread Id 由锁定线程自动设置,如果为零,或者如果非零,则使用简单的自旋等待多次重新读取锁定状态以获取锁定。如果一段时间后锁还是不可用le,它将需要升级锁(如果尚未升级),将 Thin Lock 膨胀到 Sync Block Table 和 true* 锁将需要在基于内核事件(如自动重置事件)的操作中注册。
A Thin Lock 顾名思义,它是一种重量更轻且速度更快的机制,但它是以旋转核心来实现其工作为代价的。这种 混合锁定 机制对于短期释放场景来说速度更快且效率更低,但是对于较长的争用场景,CLR 会回退到资源密集度较低的较慢内核锁。简而言之,总体而言,它通常会在日常使用中获得更好的结果。
胖锁
如果发生争用或涉及条件变量(通过 Wait、Pulse 等),则需要将额外信息存储在 Sync 中块 ,例如内核句柄 object 或与锁关联的事件列表。胖锁就像它听起来的那样,它是一种更激进的锁,它更慢但更少 resource-intensive 因为它不会围绕 CPU 不必要地旋转,它更适合更长的锁周期.
同步块Table
Object 同步块索引 header
|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 在堆上有一个pre-initialized、可回收、缓存和可重用的同步块Table。这个table可能包含一个哈希码(从header迁移而来),以及ObjectsHeader引用的各种类型的锁定信息同步块索引(提升/inflation 发生时)。
综合起来*
当调用 Monitor.Enter
时,CLR 通过将当前线程 ID(除其他事项外)存储在 object header(如前所述)或提升它来注册获取到 Sycnc 块 Table。如果有 Thin Lock,CLR 将通过检查 header 或 Sync Block Sync Block Table.
如果自旋锁在一定数量的自旋后无法获得锁,它可能最终需要向操作系统注册一个自动重置事件并将句柄存储在同步块Table。此时等待线程将只等待该句柄。
then the CLR will add the new thread to a first in first served queue and so on?
不,不存在 queue 这样的情况,随后这一切都可能导致不公平的行为。线程有能力窃取信号和唤醒之间的锁,但是 CLR 确实以有序的方式帮助实现这一点,并试图阻止 [lock convoy][3].
因此,这里显然掩盖了很多锁的类型(关键区域和条件变量)、CLR 内存模型、回调的工作方式等等。但它应该给你一个起点来回答你最初的问题
免责声明:许多信息实际上可能会发生变化,因为它们是 CLR 实现细节。
为每个锁对象使用对象实例的目的是什么? CLR 是否存储了一个线程在调用 Monitor.Enter(instance)
时传递的对象实例,以便当另一个线程试图进入锁时,CLR 将检查新线程提供的实例以及该实例是否匹配第一个线程实例,然后 CLR 会将新线程添加到先到先服务队列中,依此类推?
Is the CLR storing an instance of an object passed by a thread on a call to Monitor.Enter(instance) so that when another thread tries to enter a lock, the CLR will check the instance provided by the new thread and if the instance matches to the first threads instance then the CLR will add the new thread to a first in first served queue and so on?
不考虑抖动执行的指令重新排序和其他魔法。
首先,让我们解决问题的重要部分:
Why Do Locks Require Instances In C#?
答案不是那么令人满意,但归结为......好吧,它必须以某种方式完成!
您可以想象 C# 规范 和 CLR 可以使用 魔术字符串 , 或 number 来跟踪线程同步,但设计者选择使用 Reference Types。 引用类型 已经有一个 header 用于其他 CLR activity,所以不用给你 magic numbers 或 strings 保存在 table 中,他们选择了 双重用途 header用于 引用类型 以跟踪 线程同步 。故事基本结束。
更长的故事
Monitor
锁objects需要引用类型。 值类型没有像引用类型那样的header,部分原因是它们不需要完成并且不能被固定GC。此外,值类型可以盒装,这基本上意味着它们被包装成object.当您将 值类型 传递给 Monitor
时,它们会得到 盒装 ,当您将 相同 value type 它们被装箱到不同的 object(这否定了 lock 的所有内部 CLR 管道)。
这就是为什么值类型不能用于锁定的主要原因...
我们继续前进
值类型和引用类型都有内部内存布局。但是,引用类型 还包含 32 位 header 以帮助 CLR 执行某些 内务处理 object 上的任务(如上所述)。这就是我们要说的
header 中发生了一些事情,但它远非火箭科学。虽然,关于锁定,这里只有 2 个概念很重要,header Lock State 信息或 header 是否需要膨胀到 同步块Table.
Object header
典型 object header 格式中的最高有效字节如下所示。
|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以及object的锁定状态。但是,因为 object header(32 位)中的大小有限,所以 header 可能需要 膨胀 到 同步块 Table。这通常会在以下情况下完成。
- 已生成哈希码并已获取精简锁。
- 获得发锁
- 涉及条件变量(通过 Wait、Pulse 等)
header 不够大。
锁定状态
在 object 上创建 lock 后,CLR将查看 header 并首先确定它是否需要在 同步块 table 中找到任何锁定信息,它通过查看被设置。如果没有 Thin Lock,它将创建一个(如果适用)。如果有 Thin Lock 它会尝试旋转并等待它。如果 header 已膨胀,它将在同步块 Table 中查找锁定信息(待续...)。
锁定有两种不同的风格。 关键区域和条件变量。
- 临界区是
Enter
、Exit
、Lock
等 的结果
- 条件变量是
Wait
、Pulse
等的结果,这是另外一个故事,因为它与问题无关。
关于临界区,CLR 可以通过两种主要方式为其锁定。 瘦锁,以及胖锁。 CLR 在混合锁模型中使用这两者,这基本上意味着它先尝试一个然后回退到下一个。
薄锁
Object 薄锁 Header
|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
A Thin Lock 基本上由 App Domain Index、Recursion Level 和托管线程 ID。 Thread Id 由锁定线程自动设置,如果为零,或者如果非零,则使用简单的自旋等待多次重新读取锁定状态以获取锁定。如果一段时间后锁还是不可用le,它将需要升级锁(如果尚未升级),将 Thin Lock 膨胀到 Sync Block Table 和 true* 锁将需要在基于内核事件(如自动重置事件)的操作中注册。
A Thin Lock 顾名思义,它是一种重量更轻且速度更快的机制,但它是以旋转核心来实现其工作为代价的。这种 混合锁定 机制对于短期释放场景来说速度更快且效率更低,但是对于较长的争用场景,CLR 会回退到资源密集度较低的较慢内核锁。简而言之,总体而言,它通常会在日常使用中获得更好的结果。
胖锁
如果发生争用或涉及条件变量(通过 Wait、Pulse 等),则需要将额外信息存储在 Sync 中块 ,例如内核句柄 object 或与锁关联的事件列表。胖锁就像它听起来的那样,它是一种更激进的锁,它更慢但更少 resource-intensive 因为它不会围绕 CPU 不必要地旋转,它更适合更长的锁周期.
同步块Table
Object 同步块索引 header
|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 在堆上有一个pre-initialized、可回收、缓存和可重用的同步块Table。这个table可能包含一个哈希码(从header迁移而来),以及ObjectsHeader引用的各种类型的锁定信息同步块索引(提升/inflation 发生时)。
综合起来*
当调用 Monitor.Enter
时,CLR 通过将当前线程 ID(除其他事项外)存储在 object header(如前所述)或提升它来注册获取到 Sycnc 块 Table。如果有 Thin Lock,CLR 将通过检查 header 或 Sync Block Sync Block Table.
如果自旋锁在一定数量的自旋后无法获得锁,它可能最终需要向操作系统注册一个自动重置事件并将句柄存储在同步块Table。此时等待线程将只等待该句柄。
then the CLR will add the new thread to a first in first served queue and so on?
不,不存在 queue 这样的情况,随后这一切都可能导致不公平的行为。线程有能力窃取信号和唤醒之间的锁,但是 CLR 确实以有序的方式帮助实现这一点,并试图阻止 [lock convoy][3].
因此,这里显然掩盖了很多锁的类型(关键区域和条件变量)、CLR 内存模型、回调的工作方式等等。但它应该给你一个起点来回答你最初的问题
免责声明:许多信息实际上可能会发生变化,因为它们是 CLR 实现细节。