2011-11-25 40 views
42

我试图lock一个Boolean变量时,我遇到了以下错误:为什么我们不能锁定值类型?

“布尔”不是所要求的lock语句引用类型

似乎只有引用类型允许在lock声明,但我不知道我明白为什么。

安德烈亚斯是说明在他comment

当[值类型]对象从一个线程传递到其他,复制制成,这样的螺纹最终在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这个问题。我的问题不是怎么而是为什么

+0

您的程序没有在它使用共享副本的线程之间传递x。但是它仍然不是线程安全的,因为您不在锁的范围内访问x,并且x没有声明为volatile。我感到另一个问题:“为什么在这个例子中x需要变化?” –

+0

@MartinBrown:我知道,在我的* real *代码中,我对专用对象使用了一个锁(正如我在我的问题中提到的,它与* why *而不是* how *有关)。至于'volatile',它[在正确锁定时不需要](http://blogs.msdn.com/b/ericlippert/archive/2011/06/16/atomicity-volatility-and-immutability-are-different-部分three.aspx)。 – Otiel

回答

32

这里只是胡乱猜测......

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

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

+3

关键在于值类型每次都会装入不同的对象中(装箱与复制并不完全相同,我认为OP和未来的读者都值得注意)。请参阅[我的回答](http://stackoverflow.com/q/8267344/593627) –

+0

这是一个很好的观点。我会编辑。 –

+0

锁定值类型时,编译器可以创建不可见的固定引用类型。 – drowa

15

它扩展为:

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。有关更多信息,请参阅概念主题监视器。

+1

为什么'Monitor.Enter'和'Monitor.Exit'需要一个引用类型? (这似乎是一个显而易见的问题,因为这是真正的OP后)。 – Oded

+0

对不起,刚编辑过 –

+0

没有经过编译器检查,它没有...它可能需要一个引用类型才能正常工作,如果值是一个盒装值类型,它可能会引发异常我还没有检查。但它编译得很好。由编译器完成的引用类型检查仅适用于lock()语句。 –

2

因为值类型没有锁定语句用来锁定对象的同步块。只有参考类型携带类型信息,同步块等的开销。

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

这可能会工作(虽然这是完全没有意义的,我还没有尝试过)

int x = 7; 
object boxed = (object)x; 

//thread1: 
lock (boxed){ 
... 
} 
//thread2: 
lock(boxed){ 
... 
} 

只要每个人都使用盒装和盒装的对象,一旦你可能会得到正确的锁定,因为你只设置锁定在盒装物体上,并且它只被创建一次。不要这样做..这只是一个思考练习(甚至可能不工作 - 就像我说的,我没有测试过)。

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

+0

感谢您回答我的第二个问题。对我来说听起来很奇怪,每个线程都复制了值。 – Otiel

1

以下是从MSDN采取:

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

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

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

4

如果你问概念为什么这是不允许的,我会说答案的事实,值类型的身份是完全等同于它的(这是什么使得它的值茎类型)。

因此,任何人在宇宙中谈论int4任何地方都在谈论同样的事情 - 你怎么那么可以声称独家获得锁定呢?

+0

这是一个有趣的观点;特别是如果有人问,“所以,因为值类型在传递给对象参数时会被装箱,所以为什么不让锁也接受特定的值类型?”......因为要这样做,锁就是值 - 不是变量。那将是无用的。 +1 –

1

根据这一MSDN Thread,改变为参考变量可能是不可见的所有线程,他们可能最终使用的过时的值,而据我所知,我认为值类型不进行复印时,他们线程之间传递。

从MSDN

正是引用

同样重要的是澄清事实的任务是原子 并不意味着写立即被其他线程 观察。如果引用不是易失性的,那么 另一个线程可能会在您的线程更新它后的某个时间从 中读取一个陈旧值。然而,更新本身是 保证是原子的(你不会看到底层 指针的一部分被更新)。

+3

值类型在被不同线程使用时不会被复制。这是在memry中的相同地址 - 你可能会得到陈旧值的原因是你在一个线程中设置的值可能不会立即写回到内存位置 - 它可能被“缓存”在一个寄存器中,因为JIT可以看到它即将再次使用。 –

+0

谢谢,我不知道,我喜欢SO – Vamsi

0

我认为这是其中的答案为什么是“因为微软工程师以这种方式实现它”的答案。

锁的工作方式是通过在内存中创建一个锁结构表,然后使用对象vtable来记住所需锁的表中的位置。这表明每个对象实际上都没有锁定。只有那些被锁定的人才能做到。由于值类型没有参考,因此没有用于存储锁定位置的vtable。

为什么微软选择这种奇怪的做事方式是任何人的猜测。他们可以让Monitor成为一个你必须实例化的类。我确定我看过一位MS员工的文章,表示反思这种设计模式是一个错误,但我现在似乎无法找到它。

+2

我相信微软这样做的原因是,一个可实例化的锁定类型可能会:(1)需要终结器,(2)如果输入但没有遗漏,或(3)如果放弃,需要其他GC支持进行清理。我的猜测是,MS决定采用方法#3,并且以这样的方式实现它,即每个类对象都会平均施加成本,因此在锁定每个类对象时没有任何技术障碍。 – supercat

25

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

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

  • 指向一个类型
  • 堆同步块根
  • 指向实例数据
4

我想知道为什么。净团队决定限制开发并允许Monitor仅在参考上运行。首先,你认为锁定System.Int32而不是为了锁定目的而定义专用对象变量是很好的,这些储物柜通常不会做任何其他事情。

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

用两个词,值类型变量出现在代码中,仅在每个表达式中被评估为它们的实际值 - 仅此而已。

我想这是其中的一个要点。

+2

+1解释简单,你也回答了@马丁棕色 – dotnetguy

相关问题