.NET C # небезопасно / исправлено, не закрепляет проходной элемент массива? - PullRequest
3 голосов
/ 08 апреля 2011

У меня есть некоторый параллельный код, который имеет прерывистый сбой, и я уменьшил проблему до двух случаев, которые кажутся идентичными, но где один отказывает, а другой нет.

Сейчас я потратил слишком много времени, пытаясь создать минимальный, полный пример, который потерпел неудачу, но безуспешно, поэтому я просто публикую строки, которые терпят неудачу, на случай, если кто-нибудь увидит очевидную проблему.

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, и похоже, что происходит то, что объект Node в позиции массива 33 перемещается (в очень редких случаях), несмотря на тот факт, что мы удерживаем указатель на тип значения внутри него.

Как будто CLR не замечает, что мы передаем через объект кучи (подвижный) - элемент массива - для доступа к типу значения. Версия 'OK' никогда не выходила из строя при расширенном тестировании на 8-сторонней машине, но альтернативный путь каждый раз быстро заканчивался неудачей.

  • Разве это никогда не должно работать, и версия «ОК» слишком упрощена, чтобы проваливаться при стрессе?
  • Нужно ли самому прикреплять объект с помощью GCHandle (я заметил в IL, что только оператор fixed не делает этого)?
  • Если здесь требуется ручное закрепление, почему компилятор разрешает доступ через объект кучи (без закрепления) таким образом?

примечание: этот вопрос не обсуждает элегантность переосмысления типа blittable значения противным образом, поэтому, пожалуйста, не критикуйте этот аспект кода, если он не имеет непосредственного отношения к рассматриваемой проблеме .. спасибо

[править: joted asm] Благодаря ответу Ганса, я лучше понимаю, почему джиттер помещает вещи в стек в том, что в противном случае выглядит как пустые операции asm. См. [Rsp + 50h], например, и как он обнуляется после «фиксированной» области. Остается нерешенным вопрос: достаточно ли [cur + 18h] (строки 207-20C) в стеке для защиты доступа к типу значения способом, который не достаточен для [temp + 33 * IntPtr.Size + 18h] (строка 24А).

enter image description here

[править]

краткое изложение выводов, минимальный пример

Сравнивая два фрагмента кода ниже, я теперь считаю, что # 1 не в порядке, тогда как # 2 приемлемо.

(1.) Следующее завершается с ошибкой (по крайней мере для x64 jit); GC по-прежнему может перемещать экземпляр MyClass , если вы попытаетесь исправить его in-situ через ссылку на массив. В стеке нет места для ссылки на конкретный экземпляр объекта (элемент массива, который необходимо исправить) для публикации, чтобы 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 (без закрепления), если вы предоставите явную ссылку на стек, который может быть объявлен в ГК:

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 ]

3 голосов
/ 08 апреля 2011

Изменение объектов управляемого типа с помощью фиксированных указателей может привести к неопределенному поведению
(спецификация языка C #, глава 18.6.)

Ну, вы делаете именно это. Несмотря на словоблудие в спецификации и библиотеке MSDN, ключевое слово fixed фактически не делает объект неподвижным, оно не закреплено. Вы, наверное, узнали об этом, глядя на ИЛ. Он использует хитрый трюк, генерируя указатель + смещение и позволяя сборщику мусора корректировать указатель. У меня нет хорошего объяснения, почему это не удается в одном случае, но не в другом. Я не вижу принципиальной разницы в сгенерированном машинном коде. Но тогда я, вероятно, также не воспроизвел ваш точный машинный код, фрагмент не очень хорош.

Насколько я могу судить, в обоих случаях произойдет сбой из-за доступа к элементу структуры. Это приводит к сворачиванию указателя + смещения в один указатель с инструкцией LEA, не позволяя сборщику мусора распознать ссылку. Структуры всегда были проблемой для джиттера. Возможно, нить может объяснить разницу.

Вы можете опубликовать на connect.microsoft.com для второго мнения. Однако будет сложно ориентироваться в нарушении спецификации. Если моя теория верна, то и чтение может потерпеть неудачу, однако гораздо труднее доказать это.

Исправьте это, фактически закрепив массив с помощью GCHandle.

0 голосов
/ 08 апреля 2011

Размышляя над этим, и я предполагаю, что здесь, похоже, компилятор берет & temp (фиксированный указатель на массив tmp) и затем индексирует это с помощью [33].Таким образом, вы закрепляете временный массив, а не узел.Попробуйте ...

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