6

Imagine this C# code in some method:

SomeClass.SomeGlobalStaticDictionary = new Dictionary<int, string>()
{
    {0, "value"},
};

Let's say no one is using any explicit memory barriers or locking to access the dictionary.

If no optimization takes place, then the global dictionary should be either null (initial value) or a properly constructed dictionary with one entry.

The question is: Can the effect of the Add call and assigning to SomeGlobalStaticDictionary be reordered such that some other thread would see an empty non-null SomeGlobalStaticDictionary (or any other invalid partially constructed dictionary?)

Does the answer change if SomeGlobalStaticDictionary is volatile?

After reading http://msdn.microsoft.com/en-us/magazine/jj863136.aspx (and also its second part) I learned that in theory just because one variable is assigned in source code other threads might see it differently due to many reasons. I looked at the IL code but the question is whether the JIT compiler and/or CPU are allowed to not "flush" the effect of the Add call to other threads before the assignment of the SomGlobalStaticDictionary.

4

2 回答 2

6

在局部变量中,启用优化后,编译器(至少有时)编译为首先分配给变量的代码,然后调用Add(或为对象初始化器设置属性)。

如果您使用静态或实例变量,您会看到不同的行为:

class Test
{
    static List<int> StaticList = new List<int> { 1 };
    List<int> InstanceList = new List<int> { 2 };
}

给出以下类型初始化器 IL:

.method private hidebysig specialname rtspecialname static 
        void  .cctor() cil managed
{
  // Code size       21 (0x15)
  .maxstack  2
  .locals init (class [mscorlib]System.Collections.Generic.List`1<int32> V_0)
  IL_0000:  newobj     instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  ldc.i4.1
  IL_0008:  callvirt   instance void class [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)
  IL_000d:  nop
  IL_000e:  ldloc.0
  IL_000f:  stsfld     class [mscorlib]System.Collections.Generic.List`1<int32> Test::StaticList
  IL_0014:  ret
} // end of method Test::.cctor

以及以下构造函数 IL:

.method public hidebysig specialname rtspecialname 
        instance void  .ctor() cil managed
{
  // Code size       29 (0x1d)
  .maxstack  3
  .locals init (class [mscorlib]System.Collections.Generic.List`1<int32> V_0)
  IL_0000:  ldarg.0
  IL_0001:  newobj     instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  ldc.i4.2
  IL_0009:  callvirt   instance void class [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)
  IL_000e:  nop
  IL_000f:  ldloc.0
  IL_0010:  stfld      class [mscorlib]System.Collections.Generic.List`1<int32> Test::InstanceList
  IL_0015:  ldarg.0
  IL_0016:  call       instance void [mscorlib]System.Object::.ctor()
  IL_001b:  nop
  IL_001c:  ret
} // end of method Test::.ctor

在这两种情况下,集合都是在设置字段之前填充的。现在这并不是说可能仍然没有内存模型问题,但这与将字段设置为引用空集合然后进行Add调用不同。从分配线程的角度来看,分配发生在Add.

通常,对象初始化器和集合初始化器表达式都等效于使用临时变量构造对象 - 因此,在您在赋值中使用它的情况下,属性设置器都会在赋值发生之前被调用。

但是,我不相信针对对象/集合初始化程序的其他线程的可见性提供任何特殊保证。我建议您想象一下如果根据规范“长期”写出代码会是什么样子,然后从那里推理。

对静态初始化器和构造器有保证——但主要是在 Microsoft 的 .NET 实现中,而不是“一般”保证(例如,在 C# 规范或 ECMA 规范中)。

于 2013-04-22T16:26:04.703 回答
4

首先让我说我不知道​​您的问题的答案,但我可以帮助您将其简化为本质:

unsafe class C
{
    static int x;  // Assumed to be initialized to zero
    static int *p; // Assumed to be initialized to null
    static void M()
    {
        int* t = &C.x;
        *t = 1;
        C.p = t;
    }
    ...

这里int代表字典,p代表引用字典的字段,t是临时创建的,向字典中添加元素被建模为改变 field 的值x。所以这里的事件顺序是:获取字典的存储并将其保存在一个临时文件中,然后改变所引用的事物,然后发布结果。

问题是在 C# 内存模型下,是否允许另一个线程上的观察者看到C.p指向x并且x仍然为零的对象。

就像我说的,我不确定答案;我很想知道。

不过,我的脑海中浮现:为什么不可能呢?p并且x可以在完全不同的内存页面上。假设在某个处理器上, 的值x已经被预取但p还没有。该处理器是否可以观察到p它不为空但x仍为零?是什么阻止了它?

于 2013-04-22T17:18:18.963 回答