首页 > 解决方案 > 为什么锁需要 C# 中的实例?

问题描述

为每个锁对象使用一个对象实例的目的是什么?CLR 是否存储了一个线程在调用时传递的对象的实例,Monitor.Enter(instance)以便当另一个线程尝试进入锁时,CLR 将检查新线程提供的实例,如果该实例与第一个线程实例匹配,那么CLR 会将新线程添加到先服务队列中,等等?

标签: c#locking

解决方案


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结果
  • 条件变量是等的结果WaitPulse这是另一个故事,因为它与问题无关。

关于关键区域,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 IndexRecursion LevelManaged 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 实现细节。


推荐阅读