【问题标题】:Why can't we lock on a value type?为什么我们不能锁定一个值类型?
【发布时间】:2011-11-25 09:58:37
【问题描述】:

当我遇到以下错误时,我试图 lock Boolean 变量:

'bool' 不是 lock 语句所要求的引用类型

lock 语句中似乎只允许引用类型,但我不确定我理解为什么。

安德烈亚斯在他的comment

当 [a value type] 对象从一个线程传递到另一个线程时,会生成一个副本,因此线程最终会处理 2 个不同的对象,这是安全的。

这是真的吗?这是否意味着当我执行以下操作时,我实际上是在修改xToTruexToFalse 方法中的两个不同的x

public static class Program {

    public static Boolean x = false;

    [STAThread]
    static void Main(string[] args) {

        var t = new Thread(() => xToTrue());
        t.Start();
        // ...
        xToFalse();
    }

    private static void xToTrue() {
        Program.x = true;
    }

    private static void xToFalse() {
        Program.x = false;
    }
}

(仅此代码在其状态下显然无用,仅用于示例)


P.S:我在How to properly lock a value type 上知道这个问题。我的问题与如何无关,而与为什么有关。

【问题讨论】:

  • 您的程序没有在它使用共享副本的线程之间传递 x。但是它仍然不是线程安全的,因为您没有在锁的范围内访问 x 并且 x 没有被声明为 volatile。我觉得另一个问题来了:“为什么这个例子中的 x 需要是 volatile 的?”
  • @MartinBrown:我知道,并且在我的 real 代码中,我在专用对象上使用了锁(正如我在问题中提到的,它与 为什么而不是如何)。至于volatile,则是not needed when locking properly

标签: c# multithreading locking reference


【解决方案1】:

这里只是一个疯狂的猜测......

但是如果编译器让你锁定一个值类型,你最终什么都不会锁定......因为每次你将值类型传递给lock,你都会传递一个装箱的副本;不同的盒装副本。所以锁就好像它们是完全不同的对象。 (因为,它们实际上是)

请记住,当您为 object 类型的参数传递值类型时,它会被装箱(包装)到引用类型中。这使得每次发生这种情况时它都是一个全新的对象。

【讨论】:

  • 关键是值类型每次都会被装箱到不同的对象中(装箱与复制并不完全相同,我认为对于 OP 和未来的读者来说这是值得注意的)。见my answer
  • 编译器在锁定值类型时会创建一个不可见的固定引用类型。
  • 在 csharp 中,与 Java 不同,boolBoolean 的人是一样的吗?这很烦人。
【解决方案2】:

您无法锁定值类型,因为它没有 sync root 记录。

锁定由 CLR 和 OS 内部机制执行,这些机制依赖于具有一次只能由单个线程访问的记录的对象 - 同步块根。任何引用类型都会有:

  • 指向类型的指针
  • 同步块根
  • 指向堆中实例数据的指针

【讨论】:

    【解决方案3】:

    它扩展为:

    System.Threading.Monitor.Enter(x);
    try {
       ...
    }
    finally {
       System.Threading.Monitor.Exit(x);
    }
    

    虽然它们可以编译,但Monitor.Enter/Exit 需要引用类型,因为每次都会将值类型装箱到不同的对象实例,因此每次调用 EnterExit 都会对不同的对象进行操作.

    来自 MSDN Enter method 页面:

    使用 Monitor 来锁定对象(即引用类型),而不是值类型。当您将值类型变量传递给 Enter 时,它将被装箱为对象。如果再次将相同的变量传递给 Enter,则将其装箱为单独的对象,并且线程不会阻塞。在这种情况下,Monitor 应该保护的代码不受保护。此外,当您将变量传递给 Exit 时,还会创建另一个单独的对象。因为传递给 Exit 的对象与传递给 Enter 的对象不同,Monitor 会抛出 SynchronizationLockException。有关详细信息,请参阅概念性主题监视器。

    【讨论】:

    • 为什么Monitor.EnterMonitor.Exit 需要引用类型? (这似乎是一个显而易见的问题,因为这确实是 OP 所追求的)。
    • 不是当编译器检查它没有...它可能需要一个引用类型才能正常工作,如果该值是一个装箱的值类型,它甚至可能抛出异常我没有检查。但它编译得很好。编译器做的引用类型检查只针对lock()语句。
    • @AndersForsgren:编辑澄清。
    • 这些天它实际上也扩展为在 try{} 中包含输入。抱歉,现在只是在吹毛求疵。
    【解决方案4】:

    我想知道为什么 .Net 团队决定限制开发人员并允许 Monitor 仅对引用进行操作。首先,您认为锁定System.Int32 会更好,而不是仅仅为了锁定目的而定义一个专用的对象变量,这些储物柜通常不会做任何其他事情。

    但是看起来该语言提供的任何功能都必须具有强大的语义,而不仅仅是对开发人员有用。因此,值类型的语义是,每当值类型出现在代码中时,它的表达式就会被评估为一个值。所以,从语义的角度来看,如果我们写 `lock(x)' 并且 x 是一个原始值类型,那么它就像我们说“锁定一个关键代码块反对变量 x 的值”听起来更肯定比奇怪:)。同时,当我们在代码中遇到 ref 变量时,我们习惯于认为“哦,它是对对象的引用”,并暗示该引用可以在代码块、方法、类甚至线程和进程之间共享,因此可以作为守卫。

    简而言之,值类型变量出现在代码中只是为了在每个表达式中被评估为它们的实际值——仅此而已。

    我想这是要点之一。

    【讨论】:

    • +1 解释简单,你也回答了@martin brown
    • 也许像lock(ref value) 这样的东西有意义?
    【解决方案5】:

    如果你问概念上为什么不允许这样做,我会说答案源于值类型的 identity 完全等同于它的 value(这就是使它成为值类型的原因)。

    所以宇宙中任何地方谈论int4 的人都在谈论同样的事情 - 那么你怎么可能声称拥有独占访问权来锁定它?

    【讨论】:

    • 这是一个有趣的观点;特别是如果有人问,“既然值类型在传递给对象参数时被装箱,为什么不让锁也接受特定的值类型呢?”......因为这样做,锁将在值上 - 不是变量。那将毫无用处。 +1
    【解决方案6】:

    因为值类型没有锁语句用来锁定对象的同步块。只有引用类型会产生类型信息、同步块等的开销。

    如果你装箱你的引用类型那么你现在有一个包含值类型的对象并且可以锁定该对象(我期望)因为它现在具有对象具有的额外开销(指向用于同步块的指针锁定,指向类型信息的指针等)。正如其他人所说的那样 - 如果您将一个对象装箱,那么每次装箱时都会得到一个新对象,因此您每次都会锁定不同的对象 - 这完全违背了锁定的目的。

    这可能会起作用(尽管它完全没有意义,我还没有尝试过)

    int x = 7;
    object boxed = (object)x;
    
    //thread1:
    lock (boxed){
     ...
    }
    //thread2:
    lock(boxed){
    ...
    }
    

    只要每个人都使用 boxed 并且 boxed 对象仅设置一次,您可能会获得正确的锁定,因为您正在锁定 boxed 对象并且它只被创建一次。但不要这样做.. 这只是一个思考练习(甚至可能不起作用 - 就像我说的,我还没有测试过)。

    关于您的第二个问题 - 不,不会为每个线程复制该值。两个线程将使用相同的布尔值,但不能保证线程看到它的最新值(当一个线程设置该值时,它可能不会立即写回内存位置,因此读取该值的任何其他线程都会得到“旧”结果)。

    【讨论】:

    • 感谢您回答我的第二个问题。这对我来说听起来很奇怪,为每个线程复制值。
    【解决方案7】:

    以下摘自MSDN:

    lock (C#) 和 SyncLock (Visual Basic) 语句可用于确保代码块运行完成而不会被其他线程中断。这是通过在代码块期间为给定对象获取互斥锁来实现的。

    提供给lock关键字的参数必须是基于引用类型的对象,用于定义锁的范围。

    我认为这部分是因为锁机制使用该对象的实例来创建互斥锁。

    【讨论】:

      【解决方案8】:

      根据这个MSDN Thread,对引用变量的更改可能对所有线程都不可见,并且它们最终可能会使用陈旧的值,并且我认为值类型在线程之间传递时确实会复制。

      完全引用MSDN

      澄清分配是原子的这一事实也很重要 并不意味着写入会立即被其他人观察到 线程。如果参考不是易失性的,那么它可能是 另一个线程在某个时间从引用中读取过时的值 在您的线程更新后。但是,更新本身是 保证是原子的(你不会看到底层的一部分 指针正在更新)。

      【讨论】:

      • 不同线程使用时不复制值类型。它是内存中的同一个地址——你可能会得到陈旧值的原因是你在一个线程中设置的值可能不会立即写回内存位置——它可能被“缓存”在寄存器中,因为 JIT 可以看到它很快就会再次使用。
      【解决方案9】:

      我认为这是其中的一个案例,其中的答案是“因为 Microsoft 工程师以这种方式实现了它”。

      锁的工作原理是在内存中创建一个锁结构表,然后使用对象 vtable 记住所需锁在表中的位置。这给出了每个对象都有锁的外观,而实际上它们没有。只有那些被锁定的人才能这样做。由于值类型没有引用,因此没有 vtable 来存储锁定位置。

      为什么微软选择了这种奇怪的做事方式,谁也说不准。他们本可以让 Monitor 成为您必须实例化的类。我确定我看过 MS 员工的一篇文章,说经过反思,这种设计模式是一个错误,但我现在似乎找不到。

      【讨论】:

      • 我相信微软这样做的原因是一个可实例化的锁类型要么:(1)需要一个终结器,(2)如果进入但没有离开,则泄漏资源,或者(3)需要一些如果放弃,其他 GC 支持清理。我的猜测是,MS 决定采用方法 3,并以这样一种方式实现它,即每个类对象都会有同等的成本,因此允许锁定每个类对象没有技术障碍。
      猜你喜欢
      • 1970-01-01
      • 2014-11-09
      • 2010-10-09
      • 2011-03-08
      • 1970-01-01
      • 2016-03-28
      • 1970-01-01
      • 2023-03-18
      • 1970-01-01
      相关资源
      最近更新 更多