是锁定实例还是基于成员
Is locking instance or member based
我有一个关于在 C# 中锁定的问题。 c# 是否锁定对象的实例或成员。
如果我有以下代码:
lock(testVar)
{
testVar = testVar.Where(Item => Item.Value == 1).ToList();
//... do some more stuff
}
即使我将 testVar
设置为新值,c# 是否保持锁定?
所有 C# 对象都继承自 System.Object
,它本身始终包含 4 个字节,专供您使用 lock
的语法糖时使用。这称为 SyncBlock 对象。
当您使用 new
创建新对象时,在您的例子中,ToList
生成了对 List<T>
的新引用,您实际上是 覆盖 旧引用,这会使您的 lock
无效。这意味着现在多个线程可以 可能 在您的 lock
中。编译器会将您的代码转换为带有额外局部变量的 try-finally
块,以避免您开枪。
这就是为什么最好的做法是定义一个 专用的私有只读变量 来充当同步根对象,而不是使用 class 成员。这样,阅读您代码的任何人都清楚您的意图。
编辑:
有一个nice article on MSDN描述内存中的对象结构:
SyncTableEntry also stores a pointer to SyncBlock that contains useful
information, but is rarely needed by all instances of an object. This
information includes the object's lock, its hash code, any thunking
data, and its AppDomain index. For most object instances, there will
be no storage allocated for the actual SyncBlock and the syncblk
number will be zero. This will change when the execution thread hits
statements like lock(obj) or obj.GetHashCode.
它锁定表达式 (testVar
) 解析到的对象。这意味着您的代码确实存在线程竞争,因为一旦重新分配列表,其他并发线程可能会锁定 new 实例。
一个好的经验法则:只在 readonly
字段上 lock
。 testVar
显然不是...但它 可能是 ,尤其是当您使用 RemoveAll
更改现有列表而不是创建新列表时。这当然取决于 all 对 lock
.
中发生的列表的访问
但坦率地说,大多数代码不需要是线程安全的。如果代码需要线程安全,支持的使用场景必须让实现者清楚理解。
lock
表达式使用 Monitor.Enter/Monitor.Exit
转换为 try/finally
表达式。
使用一些类似于您的代码(使用 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
释放锁
因此,即使您认为您正在更改锁变量,但实际上并没有。然而,这样做会使您的代码难以阅读和混淆,因此您应该按照其他帖子的建议使用专用的锁变量。
我有一个关于在 C# 中锁定的问题。 c# 是否锁定对象的实例或成员。
如果我有以下代码:
lock(testVar)
{
testVar = testVar.Where(Item => Item.Value == 1).ToList();
//... do some more stuff
}
即使我将 testVar
设置为新值,c# 是否保持锁定?
所有 C# 对象都继承自 System.Object
,它本身始终包含 4 个字节,专供您使用 lock
的语法糖时使用。这称为 SyncBlock 对象。
当您使用 new
创建新对象时,在您的例子中,ToList
生成了对 List<T>
的新引用,您实际上是 覆盖 旧引用,这会使您的 lock
无效。这意味着现在多个线程可以 可能 在您的 lock
中。编译器会将您的代码转换为带有额外局部变量的 try-finally
块,以避免您开枪。
这就是为什么最好的做法是定义一个 专用的私有只读变量 来充当同步根对象,而不是使用 class 成员。这样,阅读您代码的任何人都清楚您的意图。
编辑:
有一个nice article on MSDN描述内存中的对象结构:
SyncTableEntry also stores a pointer to SyncBlock that contains useful information, but is rarely needed by all instances of an object. This information includes the object's lock, its hash code, any thunking data, and its AppDomain index. For most object instances, there will be no storage allocated for the actual SyncBlock and the syncblk number will be zero. This will change when the execution thread hits statements like lock(obj) or obj.GetHashCode.
它锁定表达式 (testVar
) 解析到的对象。这意味着您的代码确实存在线程竞争,因为一旦重新分配列表,其他并发线程可能会锁定 new 实例。
一个好的经验法则:只在 readonly
字段上 lock
。 testVar
显然不是...但它 可能是 ,尤其是当您使用 RemoveAll
更改现有列表而不是创建新列表时。这当然取决于 all 对 lock
.
但坦率地说,大多数代码不需要是线程安全的。如果代码需要线程安全,支持的使用场景必须让实现者清楚理解。
lock
表达式使用 Monitor.Enter/Monitor.Exit
转换为 try/finally
表达式。
使用一些类似于您的代码(使用 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
释放锁
因此,即使您认为您正在更改锁变量,但实际上并没有。然而,这样做会使您的代码难以阅读和混淆,因此您应该按照其他帖子的建议使用专用的锁变量。