.NET C#不安全/固定不安全';t引脚通过数组元素?

.NET C#不安全/固定不安全';t引脚通过数组元素?,c#,arrays,unsafe,value-type,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 No

我有一些并发代码出现间歇性故障,我将问题简化为两种情况,两种情况看起来相同,但一种失败,另一种没有

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

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处的节点对象正在移动(在非常罕见的情况下),尽管我们在其中持有一个指向值类型的指针

就好像CLR没有注意到我们正在通过一个heap(movable)对象——数组元素——来访问值类型。在8路机器上进行扩展测试时,“OK”版本从未失败过,但备用路径每次都会快速失败

  • 难道这永远都不应该起作用,而且“OK”版本过于精简,在压力下不会失败吗
  • 我是否需要使用GCHandle自己固定对象(我在IL中注意到,仅fixed语句并不能这样做)
  • 如果这里需要手动固定,为什么编译器允许以这种方式通过堆对象(无固定)进行访问
注意:这个问题并不是在讨论以一种令人讨厌的方式重新解释blittable值类型的优雅性,所以请不要批评代码的这一方面,除非它与手头的问题直接相关。。谢谢

[编辑:jitted asm] 多亏了Hans的回答,我更好地理解了为什么抖动会在看似空洞的asm操作中把东西放在堆栈上。例如,请参见[rsp+50h],以及它在“固定”区域之后是如何被置零的。剩余的未解决问题是堆栈上的[cur+18h](第207-20C行)是否足以以不足以[temp+33*IntPtr.Size+18h](第24A行)的方式保护对值类型的访问

[编辑]

结论摘要,最少的例子 比较下面的两段代码,我现在认为#1不合适,而#2可以接受

(1.)以下故障(至少在x64 jit上);如果您试图通过数组引用就地修复MyClass实例,GC仍然可以移动它。堆栈上没有地方可供发布特定对象实例(需要固定的数组元素)的引用,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.)但是,如果您在堆栈上提供了一个明确的引用,并且该引用可以发布给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;
struct MyValueType{public int foo;};
类MyClass{public MyValueType mvt;};
MyClass[]rgo=新的MyClass[2000];

MyClass mc=&rgo[1234];// 对此感到困惑,我在这里猜测,编译器似乎正在获取&temp(指向tmp数组的固定指针),然后使用[33]对其进行索引。因此,您要固定临时数组,而不是节点。试试看

fixed (MyValueType* pe = &(temp[33]).x)
    *(long*)pe = *(long*)&e;
通过固定指针修改托管类型的对象可能会导致未定义的行为
(C#语言规范,第18.6章)

好吧,你就是这么做的。尽管规范和MSDN库中有详细说明,但fixed关键字实际上并不会使对象不可移动,它不会被固定。你可能是通过查看IL发现的。它使用了一个巧妙的技巧,生成指针+偏移量,并让垃圾收集器调整指针。我没有一个很好的解释为什么这在一个案例中失败,而在另一个案例中却没有。我看不出生成的机器代码有什么根本区别。但是我可能也没有复制你的机器代码,代码片段不是很好

据我所知,这两种情况下都应该失败,因为结构成员访问。这会导致指针+偏移量塌陷为带有LEA指令的单个指针,从而阻止垃圾收集器识别引用。结构一直是抖动的麻烦。线程计时或许可以解释这种差异

您可以发布到connect.microsoft.com以获取第二种意见。然而,围绕规范冲突进行导航将是困难的。如果我的理论是正确的,那么阅读也可能失败,尽管要证明这一点要困难得多


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

我想我的问题可以归结为“fixed”语句的细节。它实际上保证了什么,结果是什么被放入IL代码中?我应该在IL中看到对GCHandle.Alloc的显式调用吗?谢谢你的建议。我试过了,结果也一样;仍然会立即崩溃。非常好的回答,谢谢。在开始意识到GC需要在堆栈上嗅探活动引用之后,我回到了我的帖子,上面的代码没有公开它们。我认为立即失败的原因是,阵列版本在检查索引超出范围时,在未受保护的间隙中有更多的指令。只有3条指令——再加上运气——可能会允许另一条指令工作。顺便说一句,fwiw我在测试gcServer(8路x64)上的发布版本@Glenn,@Hans:我有点困惑。。。这是来自规范:“对于由固定指针初始值设定项计算的每个地址,fixed语句确保地址引用的变量在fixed语句期间不会被垃圾收集器重新定位或处理。例如,如果由固定指针初始值设定项计算的地址引用对象的字段或数组实例的元素,则固定语句保证在语句的生存期内不会重新定位或处理包含该对象的实例。“这难道不应该吗?@Mehrdad:是的,用新的代码片段检查一下我刚才重新编辑的结论;我现在相信#2 e