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

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

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

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行)。

在此处输入图像描述

[编辑]

结论摘要,最小的例子

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

(1.)以下失败 (至少在x64 jit上); 如果您尝试通过数组引用在原位修复它 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.)但是,如果您在堆栈上提供可以通告给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; 

这是我留下的地方,除非有人可以提供更正或更多信息……

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

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

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

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

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

令人困惑的是,我在这里猜测,看起来编译器正在采用&temp(固定指向tmp数组的指针),然后使用[33]对其进行索引。 所以你要固定临时数组,而不是节点。 尝试…

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