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