使用集合初始化程序时,另一个线程可以看到部分创建的集

时间:2013-04-22 16:20:44

标签: c# thread-safety local-variables collection-initializer

想象一下这个C#代码的某种方法:

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

假设没有人使用任何显式内存屏障或锁定来访问字典。

如果没有进行优化,则全局字典应为null(初始值)或具有一个条目的正确构造的字典。

问题是:可以重新排序Add调用和分配给SomeGlobalStaticDictionary的效果,以便其他一些线程看到一个空的非null SomeGlobalStaticDictionary(或任何其他无效的部分构造的字典?)

如果SomeGlobalStaticDictionary是易变的,答案会改变吗?

在阅读http://msdn.microsoft.com/en-us/magazine/jj863136.aspx(以及它的第二部分)后,我从理论上了解到,因为在源代码中分配了一个变量,其他线程可能会因为许多原因而看到不同。我查看了IL代码,但问题是在分配SomGlobalStaticDictionary之前,是否允许JIT编译器和/或CPU“刷新”Add调用对其他线程的影响。

2 个答案:

答案 0 :(得分: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

之后

通常,对象初始值设定项和集合初始值设定项表达式都等同于使用临时变量构造对象 - 因此,如果在赋值中使用它,则属性设置器在之前都被称为任务发生。

但是,我不相信任何特殊保证是围绕对象/集合初始值设定项的其他线程的可见性给出的。我建议你想象如果根据规范写出“长手”代码会是什么样子,然后从那里推断。

为静态初始化程序和构造函数提供了 保证 - 但主要是在.NET的Microsoft实现中而不是“一般”保证(例如在C#规范或ECMA规范中)。

答案 1 :(得分: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是临时创建的,并且向字典添加元素是建模为改变字段x的值。所以这里的事件序列是:获取字典的存储并将其保存在临时字符中,然后改变所引用的内容,然后发布结果。

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

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

尽管如此:为什么可能? px可以位于完全不同的内存页面上。假设在某些处理器上预先提取x的值,但p没有。该处理器是否可以观察到p不为空但x仍为零?什么阻止了?