我有一些并发代码,它偶尔会出现故障。我已将问题缩小到两个看起来相同但一个失败而另一个不失败的情况。
我已经花费了太多时间尝试创建一个最小化且完整的失败示例,但没有成功。因此,我只发布了失败的行,以便任何人都可以看到明显的问题。
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没有注意到我们通过堆(可移动)对象——数组元素——来访问值类型。 'OK'版本在8路机器上进行了长时间测试,从未失败过,但备选路径每次都很快失败。
这永远不应该起作用,而“OK”版本太简化,以至于在压力下不会失败吗?
我需要使用GCHandle自己固定对象吗(我注意到在IL中,“fixed”语句本身并没有这样做)?
如果此处需要手动固定,则编译器为什么允许以这种方式(无需固定)通过堆对象访问?
请注意:除非直接涉及问题,否则本问题不涉及重新解释混合值类型的优雅性,请勿批评代码方面,谢谢。
[编辑:Jitted Asm]感谢汉斯的回复,我更好地理解了jit在何时将事物放在堆栈上,似乎是空洞的asm操作。例如,请参见[rsp + 50h],以及如何在“fixed”区域之后将其清零。剩下的未解决问题是,堆栈上的[cur + 18h](第207-20C行)是否足以保护对值类型的访问,而[temp + 33 * IntPtr.Size + 18h](第24A行)则不足。
[编辑]
结论摘要,最小示例
比较下面的两个代码片段,我现在相信#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.) 但是,如果您在堆上提供了一个显式的引用,并且可以向垃圾收集器广告,即使没有固定(pinning),您也可以使用 fixed 访问(可移动)对象内部的结构:
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;
这就是我能提供的,除非有人可以提供更正或更多信息...