当我们使用锁定对象时会发生什么?我知道它运行时使用了monitor.Enter和Exit方法。但真正发生在引擎盖下的是什么? 为什么只有用于锁定的引用类型? 即使用于完成锁定的对象发生了变化,为什么还能提供线程安全性?
在当前样本中,我们正在修改用于锁定目的的对象。理想情况下,这不是一种优选的方法,最佳实践是使用专用的私有范围变量。
static List<string> stringList = new List<string>();
static void AddItems(object o)
{
for (int i = 0; i < 100; i++)
{
lock (stringList)
{
Thread.Sleep(20);
stringList.Add(string.Format("Thread-{0},No-{1}", Thread.CurrentThread.ManagedThreadId, i));
}
}
string[] listArray = null;
lock(stringList)
listArray = stringList.ToArray();
foreach (string s in listArray)
{
Console.WriteLine(s);
}
}
答案 0 :(得分:6)
引擎盖下发生的事情是这样的:
object
类型中有一个隐藏字段。Monitor.Enter()
和Monitor.Exit()
使用该字段进行相互沟通。object
继承该字段。当然,该字段的类型是特殊的:它是一个以线程安全的方式工作的同步锁。实际上,当然,它实际上并不是CLR意义上的字段,而是CLR的一个特殊功能,它在每个对象的内存中使用一块内存来实现同步锁定。 (确切的实现在MSDN杂志的“Safe Thread Synchronization”中有描述。)
为什么它仍然提供线程安全?我认为你的意思是:为什么它不会破坏线程安全的对象的线程安全?答案很简单:因为你可以拥有部分线程安全且部分不安全的对象。你可以拥有一个带有两个方法的对象,并且使用其中一个方法是线程安全的,而另一个方法则不是。无论对象的其余部分是什么,Monitor.Enter()
都是线程安全的。
为什么只有用于锁定的引用类型?因为只有引用类型实际上在它们的内存块中具有这种特殊的魔力。值类型实际上只是值本身:在int的情况下是32位整数;在自定义结构的情况下所有字段的串联。您可以将值类型传递给Monitor.Enter()
,它不会抱怨,但它不起作用,因为值类型将装箱 - 即包装到参考对象中类型。当您致电Monitor.Exit()
时,它会再次装箱 ,因此会尝试释放不同对象参考上的锁。
关于您的代码示例:我认为它没有任何问题。您对stringList
变量的所有访问都包含在lock
中,除了在初始化期间,您永远不会分配给stringList
字段。这没有什么可以出错的;它是线程安全的。 (当然,如果某些其他代码在没有锁定的情况下访问该字段,可能会出错。如果您要将该字段公开,则很有可能会意外地发生这种情况。没有必要除非你真的不能确保你不能控制的代码不能访问该变量,否则只能使用本地范围的变量进行锁定。)
答案 1 :(得分:4)
但是真正发生了什么 罩?
有关详细说明,请参阅this MSDN article。
实质上,每个分配的CLR对象都有一个包含同步块索引的关联字段。该索引指向CLR维护的同步块池。同步块保存与在同步期间使用的关键部分相同的信息。最初,对象的同步块索引没有意义(未初始化)。但是,当您锁定对象时,它会获得池中的有效索引。
为什么只使用引用类型 锁定?
您需要一个引用类型,因为值类型没有关联的同步块索引字段(开销较小)。
即使用于的对象 完成锁定改变了, 它怎么还提供线程 安全
锁定CLR对象然后对其进行修改类似于具有CRITICAL_SECTION
成员的C ++对象,该成员在修改同一对象时用于锁定。那里没有线程安全问题。
在目前的样本中,我们是 修改用于的对象 锁定的目的。理想情况下,这是 不是这样做的首选方式 最佳实践是使用专用 私有范围的变量。
正确,这种情况也在文章中描述。如果您没有使用完全控制所属类的私有范围变量,那么当两个单独的类决定对同一引用对象lock
时,您可能遇到死锁问题(例如,如果由于某种原因, stringList
被传递给另一个类,然后在其上决定lock
。这是不太可能的,但是如果你使用一个永远不会被传递的严格控制的私有范围变量,你将完全避免这种死锁。