问题描述
在多线程环境中,锁定对线程敏感的资源很重要。我经常假设集合等是线程不安全的,具体取决于 MS 文档,但简单类型是否也是线程敏感的?
让我们举个例子。锁定 int 属性访问是否有用,例如
public int SomeProperty
{
get
{
lock (_lock)
{
return _value;
}
}
}
或者是一个普通的吸气剂就足够了,即
public int SomeProperty => _value;
据我所知,一个简单的字段读取是线程安全的,但我仍然在网上和一些代码库中看到第一个例子。
第二个问题,单行指令中的值是顺序读取还是同时读取?换句话说,我这样做时是否需要锁定
public TimeSpan GetSomeExampleValue()
{
lock (_lock)
{
return _dateTime1 - _dateTime2;
}
}
或者我可以简单地做
public TimeSpan GetSomeExampleValue()
{
return _dateTime1 - _dateTime2;
}
解决方法
注意:这里的一切都基于有效锁定; lock
对抗int
(第一个示例中的lock (_value)
)是一件非常糟糕的事情,并且提供零保护;不要那样做!编译器可能已经在向您大喊大叫(您应该只锁定引用类型,否则它每次都会装箱并为您提供不同的对象,因此每次都会有不同的锁定)。
据我所知,一个简单的字段读取是线程安全的,
事情比这复杂得多。首先,你需要定义线程安全!您可能指的是三种不同的场景:
- 避免“撕裂”值 - 在写入发生时读取的不一致的单个值,并且具有损坏状态(通常)一半来自前/后值,一半来自另一个 - 创建第三个幻象值逻辑上从来不存在;在这里,我们需要考虑“原子性”——C# 语言定义的类型始终是原子安全的,包括
int
和引用,但不包括DateTime
;因此,int
示例在这里是安全的,但DateTime
值不是(但是,实际上,在 64 位进程中,您应该可以使用高达 64 位的结构,但是: C# 规范并不能保证这一点 - 但是在 CLR 规范中提到了) - 避免多个值之间的不一致状态;再次考虑
DateTime
值 - 即使我们不撕任何东西,我们也可以从更新的一侧得到_dateTime1
,从另一侧得到_dateTime2
,这意味着 {{1 }} 我们返回的是一个虚值,不代表两个字段的任何一致的逻辑状态 - 内部 CPU 优化,例如乱序读/写,这意味着我们会得到非常奇怪的结果
现在
- 如果没有
TimeSpan
,lock
仍然不会撕裂,但容易受到其他撕裂;int
易受这三种情况的影响 - 使用
DateTime
,两者都可以完全防范所有三个
您可能还希望将不变性视为一种简化逻辑的方式,但请记住,lock
是谎言 - 更多的是指南而不是一个实际的规则。