在C#中访问变量是一个原子操作吗?
我已经提出相信,如果多个线程可以访问一个变量,那么对该变量的所有读取和写入操作都必须由同步代码保护,例如“锁定”语句,因为处理器可能会在半途切换到另一个线程一个写。
但是,我使用Reflector查看System.Web.Security.Membership,发现如下代码:
public static class Membership
{
private static bool s_Initialized = false;
private static object s_lock = new object();
private static MembershipProvider s_Provider;
public static MembershipProvider Provider
{
get
{
Initialize();
return s_Provider;
}
}
private static void Initialize()
{
if (s_Initialized)
return;
lock(s_lock)
{
if (s_Initialized)
return;
// Perform initialization...
s_Initialized = true;
}
}
}
为什么s_Initialized字段在锁之外读取? 难道另一个线程不能同时写入它吗? 读取和写入变量是原子吗?
为了明确的答案去规范。 :)
分区I,CLI规范的第12.6.6节指出:“符合CLI的应保证对所有对某个位置的写入访问都是相同大小时,对正确对齐的不大于本地字大小的内存位置的读取和写入访问是原子的“。
因此,这证实了s_Initialized永远不会不稳定,读取和写入小于32位的primitve类型是原子的。
特别是, double
和long
( Int64
和UInt64
) 不能保证在32位平台上是原子的。 您可以使用Interlocked
类中的方法来保护这些方法。
此外,虽然读取和写入是原子性的,但是由于必须读取,操作和重写,所以存在与加法,减法以及增量和减量基元类型的竞争条件。 联锁类允许您使用CompareExchange
和Increment
方法保护这些类。
互锁创建了一个内存屏障,以防止处理器重新排序读取和写入。 在这个例子中锁创建了唯一必需的屏障。
这是双重检查锁定模式的一种(坏)形式,它在C#中不是线程安全的!
这个代码有一个大问题:
s_Initialized不易变。 这意味着在初始化代码中的写入可以在s_Initialized设置为true之后移动,并且即使s_Initialized为true,其他线程也可以看到未初始化的代码。 这不适用于Microsoft的框架实现,因为每个写都是易失性写入。
但是在微软的实现中,未初始化数据的读取可以被重新排序(即由CPU预取),所以如果s_Initialized为true,读取应该初始化的数据可能导致读取旧的,未初始化的数据,因为缓存命中(即读取被重新排序)。
例如:
Thread 1 reads s_Provider (which is null)
Thread 2 initializes the data
Thread 2 sets s_Initialized to true
Thread 1 reads s_Initialized (which is true now)
Thread 1 uses the previously read Provider and gets a NullReferenceException
在读取s_Initialized之前移动s_Provider的读取是完全合法的,因为在任何地方都没有易失性读取。
如果s_Initialized是易失性的,那么在读取s_Initialized之前,s_Provider的读取将不会被允许移动,并且在s_Initialized设置为true并且一切正常之后,不允许Provider的初始化移动。
Joe Duffy还写了一篇关于这个问题的文章:关于双重检查锁定的破坏变体
喋喋不休 - 标题中的问题绝对不是罗里所要求的真正问题。
有名的问题有一个“不”的简单答案 - 但是当你看到真正的问题时,这根本没有任何帮助 - 我不认为有人给出了一个简单的答案。
Rory提出的真正问题在很晚之后才提出,并且与他给出的例子更为相关。
为什么s_Initialized字段在锁之外读取?
答案也很简单,尽管与变量访问的原子性完全无关。
s_Initialized字段在锁定之外被读取,因为锁定是昂贵的 。
由于s_Initialized字段本质上是“一次写入”,它永远不会返回误报。
在锁外读取它是经济的。
这是一个低成本的活动,很有可能带来好处。
这就是为什么它在锁外读取 - 为了避免支付使用锁的费用,除非它被指出。
如果锁定便宜,代码会更简单,并省略首次检查。
(编辑:rory的良好回应如下:叶,布尔读取是非常原子的,如果有人用非原子布尔读取构建处理器,它们将在DailyWTF上显示。)
链接地址: http://www.djcxy.com/p/50081.html