是基于锁定实例还是成员

时间:2015-01-12 11:06:14

标签: c# .net multithreading locking

我有一个关于锁定c#的问题。 c#是锁定对象的实例还是成员。

如果我有以下代码:

lock(testVar)
{
    testVar = testVar.Where(Item => Item.Value == 1).ToList();
    //... do some more stuff
}

c#是否保持锁定,即使我将testVar设置为新值?

3 个答案:

答案 0 :(得分:7)

所有C#对象都继承自System.Object,当lock使用语法糖时,它本身总是包含4个字节。这称为 SyncBlock对象

使用new创建新对象时,ToList生成了对List<T>的新引用,实际上覆盖旧参考,会使您的lock无效。这意味着现在多个线程可能位于lock内。编译器会将你的代码转换为带有额外局部变量的try-finally块,以避免你射击你的腿。

这就是为什么最佳做法是定义专用私有只读变量,它将充当同步根对象,而不是使用类成员。这样,任何阅读代码的人都清楚你的意图。

修改

有一个nice article on MSDN描述了内存中的对象结构:

  

SyncTableEntry还存储指向包含有用的 SyncBlock 的指针   信息,但对象的所有实例都很少需要。这个   信息包括对象的锁,其哈希码,任何thunking   数据及其AppDomain索引。对于大多数对象实例,会有   没有为实际的SyncBlock和syncblk分配存储空间   数字将为零。当执行线程命中时,这将改变   像lock(obj)或obj.GetHashCode。

这样的语句

Object in memory representation

答案 1 :(得分:4)

它锁定表达式(testVar)解析为的对象。这意味着您的代码确实存在线程争用,因为一旦重新分配列表,其他并发线程可能会锁定 new 实例。

一个好的经验法则:lock字段上只有readonlytestVar显然不是......但可能是,特别是如果您使用RemoveAll更改现有列表而不是创建新列表。这当然取决于所有lock内发生的列表的访问权。

坦率地说,大多数代码不需要是线程安全的。如果代码 需要线程安全,那么实现者必须清楚地理解支持的使用场景。

答案 2 :(得分:1)

lock表达式使用try/finally转换为Monitor.Enter/Monitor.Exit表达式。 使用类似于您的代码进行简单测试(使用VS2015预览版),您可以看到编译器将代码转换为什么。

代码

var testVar = new List<int>();
lock (testVar)
{
    testVar = new List<int>();
    testVar.Add(1);
}

实际上是翻译成这个:

List<int> list2;
List<int> list = new List<int>();
bool lockTaken = false;
try
{
    list2 = list;
    Monitor.Enter(list2, ref lockTaken);
    list = new List<int> { 1 };
}
finally
{
    if (lockTaken)
    {
        Monitor.Exit(list2);
    }
}

因此,您可以看到编译器已完全删除了变量testVar,并将其替换为2个变量,即listlist2。然后发生以下情况:

  1. list2初始化为list,现在两个引用都指向List<int>的同一个实例。
  2. 调用Monitor.Enter(list2, ref lockTaken)List<int>对象中的同步块与当前线程相关联。
  3. list变量已分配给List<int>的新实例,但list2仍指向我们锁定的原始实例。
  4. 使用list2
  5. 释放锁定

    所以即使你认为你正在改变锁变量,你实际上并非如此。然而,这样做会使您的代码难以阅读和混淆,因此您应该使用其他帖子建议的专用锁定变量。