.NET C#unsafe / fixed不会固定直通数组元素吗?

时间:2011-04-08 02:59:11

标签: c# arrays unsafe value-type

我有一些并发故障的并发代码,我把问题减少到两个似乎相同的情况,但是哪一个失败而另一个没有。

我现在花了太多时间试图创建一个失败的最小的完整示例,但没有成功,所以我只是发布失败的行,以防任何人看到明显的问题。

Object lock = new Object();

struct MyValueType { readonly public int i1, i2; };
class Node { public MyValueType x; public int y; public Node z; };
volatile Node[] m_rg = new Node[300];

unsafe void Foo()
{
    Node[] temp;
    while (true)
    {
        temp = m_rg;
        /* ... */
        Monitor.Enter(lock);
        if (temp == m_rg)
            break;
        Monitor.Exit(lock);
    }

#if OK                                      // this works:
    Node cur = temp[33];
    fixed (MyValueType* pe = &cur.x)
        *(long*)pe = *(long*)&e;
#else                                       // this reliably causes random corruption:
    fixed (MyValueType* pe = &temp[33].x)
        *(long*)pe = *(long*)&e;
#endif

    Monitor.Exit(lock);
}

我已经研究了IL代码,看起来正在发生的事情是数组位置33处的Node对象正在移动(在非常罕见的情况下),尽管我们正在持有指向其中的值类型的指针。

就好像CLR没有注意到我们正在通过通过传递堆(可移动)对象 - 数组元素 - 来访问值类型。在8路机器上进行扩展测试时,“OK”版本从未失败,但每次都会快速失败。

  • 这是不是应该工作,'OK'版本太精简而不能在压力下失败?
  • 我是否需要使用GCHandle自己固定对象(我在IL中注意到修复语句本身没有这样做)?
  • 如果此处需要手动固定,为什么编译器允许以这种方式通过堆对象(无需固定)进行访问?

注意:这个问题并没有讨论以讨厌的方式重新解释blittable值类型的优雅,所以请不要批评代码的这个方面,除非它与手头的问题直接相关..谢谢

[编辑:jitted asm] 感谢Hans的回复,我更清楚为什么抖动会把东西放在堆栈中,看起来像是空的asm操作。例如,参见[rsp + 50h],以及它如何在“固定”区域之后被取消。剩下的未解决的问题是堆栈上的[cur + 18h](第207-20C行)是否足以保护对足够[temp + 33]的方式对值类型的访问* IntPtr.Size + 18h](第24A行)。

enter image description here

[编辑]

结论摘要,最小例子

比较下面的两个代码片段,我现在认为#1不合适,而#2是可以接受的。

(1。)以下失败(至少在x64 jit上);如果您尝试通过数组引用尝试修复 in-situ ,GC仍然可以移动 MyClass 实例。堆栈上没有地方可供发布的特定对象实例(需要修复的数组元素)的引用,以供GC注意。

struct MyValueType { public int foo; };
class MyClass { public MyValueType mvt; };
MyClass[] rgo = new MyClass[2000];

fixed (MyValueType* pvt = &rgo[1234].mvt)
    *(int*)pvt = 1234;

(2。)但是你可以使用 fixed (没有固定)来访问(可移动)对象内的结构,如果你在堆栈上提供了一个明确的引用,它可以被广告给GC:

struct MyValueType { public int foo; };
class MyClass { public MyValueType mvt; };
MyClass[] rgo = new MyClass[2000];

MyClass mc = &rgo[1234];              // <-- only difference -- add this line
fixed (MyValueType* pvt = &mc.mvt)    // <-- and adjust accordingly here
    *(int*)pvt = 1234;

除非有人可以提供更正或更多信息,否则我将离开它...

2 个答案:

答案 0 :(得分:3)

  

通过固定指针修改托管类型的对象可能导致未定义的行为
(C#语言规范,第18.6章。)

嗯,你就是这样做的。尽管规范和MSDN库中有措辞,但 fixed 关键字实际上并不会使对象无法移动,也无法固定。您可能从查看IL时发现了。它通过生成指针+偏移并让垃圾收集器调整指针来使用一个聪明的技巧。我没有一个很好的解释,为什么在一个案例中失败但在另一个案例中失败。我没有看到生成的机器代码的根本区别。但是,我可能也没有重现你的确切机器代码,该片段不是很好。

尽管我可以告诉它,但由于结构成员访问,它在两种情况下都会失败。这会导致指针+偏移量折叠为带有LEA指令的单个指针,从而阻止垃圾收集器识别引用。结构一直是抖动的麻烦。线程时间也许可以解释这种差异。

您可以发布到connect.microsoft.com获取第二个意见。然而,围绕规范违规进行导航将很困难。如果我的理论是正确的,那么读取也可能失败,但更难以证明。

通过使用GCHandle实际固定数组来修复它。

答案 1 :(得分:0)

令人费解,我猜这里,看起来编译器正在采用&amp; temp(固定指向tmp数组的指针),然后用[33]索引。所以你要固定临时数组,而不是节点。尝试...

fixed (MyValueType* pe = &(temp[33]).x)
    *(long*)pe = *(long*)&e;