我有一个关于锁定c#的问题。 c#是锁定对象的实例还是成员。
如果我有以下代码:
lock(testVar)
{
testVar = testVar.Where(Item => Item.Value == 1).ToList();
//... do some more stuff
}
c#是否保持锁定,即使我将testVar
设置为新值?
答案 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。
这样的语句
答案 1 :(得分:4)
它锁定表达式(testVar
)解析为的对象。这意味着您的代码确实存在线程争用,因为一旦重新分配列表,其他并发线程可能会锁定 new 实例。
一个好的经验法则:lock
字段上只有readonly
。 testVar
显然不是......但可能是,特别是如果您使用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个变量,即list
和list2
。然后发生以下情况:
list2
初始化为list
,现在两个引用都指向List<int>
的同一个实例。 Monitor.Enter(list2, ref lockTaken)
将List<int>
对象中的同步块与当前线程相关联。 list
变量已分配给List<int>
的新实例,但list2
仍指向我们锁定的原始实例。list2
所以即使你认为你正在改变锁变量,你实际上并非如此。然而,这样做会使您的代码难以阅读和混淆,因此您应该使用其他帖子建议的专用锁定变量。