Неустойчивые операции чтения / записи и упорядочение потоков.

Я пытаюсь обдумать тонкости, связанные с барьерами памяти и изменчивыми операциями чтения / записи. Я читаю здесь статью Джозефа Альбахари:

http://www.albahari.com/threading/part4.aspx

И спотыкаясь о том, когда нужен барьер памяти перед чтением / записью, а когда он нужен после. В этом разделе кода в разделе «полная ограда» он ставит барьер памяти после каждой записи и перед каждым чтением:

class Foo
{
  int _answer;
  bool _complete;

  void A()
  {
    _answer = 123;
    Thread.MemoryBarrier();    // Barrier 1
    _complete = true;
    Thread.MemoryBarrier();    // Barrier 2
  }

  void B()
  {
    Thread.MemoryBarrier();    // Barrier 3
    if (_complete)
    {
      Thread.MemoryBarrier();       // Barrier 4
      Console.WriteLine (_answer);
    }
  }
}

Он продолжает объяснять:

Барьеры 1 и 4 не позволяют этому примеру записать «0». Барьеры 2 и 3 обеспечивают гарантию свежести: они гарантируют, что если B выполняется после A, чтение _complete будет иметь значение true.

Вопрос № 1: У меня нет проблем с барьерами 1 и 4, поскольку это предотвратит изменение порядка через эти барьеры. Но я не совсем понимаю, зачем нужны барьеры 2 и 3. Может кто-нибудь объяснить, особенно с учетом того, как изменчивые операции чтения и записи реализованы в классе Thread (поясняется далее)?

Теперь я действительно начинаю путаться в том, что это фактическая реализация Thread.VolatileRead/Write():

[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void VolatileWrite (ref int address, int value)
{
  MemoryBarrier(); address = value;
}

[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static int VolatileRead (ref int address)
{
  int num = address; MemoryBarrier(); return num;
}

Как видите, в отличие от предыдущего примера, встроенные энергозависимые функции устанавливают барьеры памяти перед каждой записью (а не после) и после каждого чтения (а не до). Поэтому, если бы мы переписали предыдущий пример с эквивалентной версией, основанной на встроенных изменчивых функциях, вместо этого это выглядело бы так:

class Foo
{
  int _answer;
  bool _complete;

  void A()
  {
    Thread.MemoryBarrier();    // Barrier 1
    _answer = 123;
    Thread.MemoryBarrier();    // Barrier 2
    _complete = true;
  }

  void B()
  {
    if (_complete)
    {
      Thread.MemoryBarrier();    // Barrier 3
      Console.WriteLine (_answer);
      Thread.MemoryBarrier();       // Barrier 4
    }
  }
}

Вопрос № 2: Являются ли оба класса Foo функционально эквивалентными? Почему или почему нет? Если барьеры 2 и 3 (в первом классе Foo) необходимы для гарантии записи значения и чтения фактического значения, тогда не будут ли методы Thread.VolatileXXX бесполезными?

В StackOverflow есть пара похожих вопросов с принятыми ответами, например «барьер 2 гарантирует, что запись в _complete не кэшируется», но ни один из них не касается того, почему Thread.VolatileWrite() ставит барьер памяти перед записью, если это так, и почему барьер 3 является необходимо, если Thread.VolatileRead() устанавливает барьер памяти после чтения, но гарантирует актуальность значения. Я думаю, это то, что меня больше всего сбивает с толку.

ОБНОВЛЕНИЕ:

Итак, после дополнительного чтения и размышлений у меня есть теория, и я обновил исходный код атрибутами, которые, по моему мнению, могут быть актуальными. Я не думаю, что барьеры памяти в Thread.VolatileRead/Write методах существуют вообще для обеспечения «свежести» значений, а скорее для обеспечения гарантий переупорядочения. Установка барьера после чтения и перед записью гарантирует, что никакие записи не будут перемещены до любых операций чтения (но не наоборот).

Из того, что мне удалось найти, все операции записи на x86 гарантируют согласованность кеша за счет аннулирования их строки кэша на других ядрах, поэтому "свежесть" гарантирована, пока значение не кэшируется в регистре. Моя теория на пути VolatileRead/Write обеспечения того, чтобы значение не было в регистре, что может быть далеко, но я думаю, что я на правильном пути, заключается в том, что они рассчитывают на детали реализации .NET, где если они помечены MethodImplOptions.NoInlining (что, как вы можете видеть выше), тогда значение нужно будет передать в / из метода вместо того, чтобы быть встроенным как локальная переменная, и, следовательно, к нему нужно будет получить доступ из памяти / кеша, а не напрямую через регистр, что устраняет необходимость в дополнительном барьере памяти после записи и перед чтением. Понятия не имею, так ли это, но это единственный способ увидеть, как он работает правильно.

Может ли кто-нибудь подтвердить или опровергнуть, что это так?


person Mike Marynowski    schedule 12.03.2017    source источник
comment
Microsoft очень сильно облажалась. Они исправили это с помощью класса Volatile, прочтите комментарий .   -  person Hans Passant    schedule 12.03.2017
comment
@HansPassant Насколько я могу судить, методы Volatile работают быстрее с меньшими гарантиями упорядочения, но в остальном методы Thread по-прежнему гарантируют свежие чтения и тому подобное. Я обновил свой ответ своей теорией после того, как еще немного покопался - есть какие-либо дополнительные комментарии по этому поводу? Пытаюсь понять, как volatile read работает с барьером памяти ПОСЛЕ чтения.   -  person Mike Marynowski    schedule 12.03.2017


Ответы (1)


Я не думаю, что барьеры памяти в методах Thread.VolatileRead/Write существуют вообще для обеспечения «свежести» значений, а скорее для обеспечения гарантий переупорядочения.

Верно.

Установка барьера после чтения и перед записью гарантирует, что никакие записи не будут перемещены до любых операций чтения (но не наоборот).

Полный барьер памяти имеет семантику как получения, так и освобождения, он предотвращает переупорядочение как предыдущих обращений к памяти, так и последующих обращений к памяти до состояния до барьера.

Может ли кто-нибудь подтвердить или опровергнуть, что это так?

Возможно, вы правы относительно операций записи в реализации Microsoft .NET на x86, но это не относится к операциям чтения. Чтение может быть переупорядочено между предыдущими обращениями и барьером памяти либо JIT-компилятором (возможно, без атрибута no-inlining), либо CPU (даже с атрибутом no-inlining).

Однако это не должно изменить то, что видит работающий код, хотя при чтении может не отображаться значение самое свежее.

int value = 0;
bool done = false;

// in thread 1
value = 123;
Thread.VolatileWrite(ref done, true);

// in thread 2
Thread.SpinUntil(() => Thread.VolatileRead(ref done));
Console.WriteLine(value); // guaranteed 123 due to the memory barrier

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

В любом случае, я советую просто не использовать Thread.VolatileRead и Thread.VolatileWrite.

Читает и записывает в volatile поля, а методы Volatile.Read и Volatile.Write обеспечивают правильную семантику.

Хотя методы Volatile.Read и Volatile.Write реализованы в C # так же, как Thread.VolatileRead и Thread.VolatileWrite, CLR заменяет методы собственными версиями фактической изменчивой семантикой чтения / записи.

person acelent    schedule 16.03.2017
comment
Я знаю, что барьеры памяти препятствуют перемещению обоих методов, но из-за того, как написаны методы Volatile, если вы выполните Write, за которым следует Read, процессор все равно может переупорядочить их, потому что между ними нет барьера памяти, поэтому он не Не обязательно предотвращать перемещение операций чтения перед записью. - person Mike Marynowski; 17.03.2017
comment
В документации для Thread.VolatileRead указано, что он получает самое последнее значение. Вы говорите, что документация неверна? В цикле это может не иметь значения, но это может иметь большое значение в коде, который выполняет однократную проверку, так ли это. - person Mike Marynowski; 17.03.2017
comment
процессор все еще может переупорядочить их, потому что между ними нет барьера памяти - Да, но возможен любой возможный результат. Если чтение относится к той же переменной, оно будет иметь правильное значение из-за зависимости данных; если он находится в другой переменной, то (из спецификации CLR) не требуется соответствующая реализация для обеспечения единого общего упорядочивания изменчивой записи, как видно из всех потоков выполнения. Итак, несколько изменчивых записей из несколько потоков могут происходить в любом порядке. - person acelent; 17.03.2017
comment
Вы говорите, что документация неверна? - Да (и я не единственный, чего стоит), или, по крайней мере, это зависит от определения свежести , который не обязательно является текущим или последним на момент инструкции чтения. это может иметь большое значение в коде, который выполняет однократную проверку - если вы не синхронизируете (или иным образом не обнаруживаете изменчивые изменения), в любом случае нет правильного результата. Какой правильный случай зависит от такой одноразовой проверки? - person acelent; 17.03.2017
comment
Я знаю, что это разрешено. Вы указали, что установка барьера после чтения и перед записью гарантирует, что никакие записи не будут перемещены до любых операций чтения (но не наоборот), и ответили на вопросы о барьерах памяти, не позволяющих переупорядочить оба доступа к памяти. У нас нет разногласий ни в чем, я просто показываю вам, насколько мое утверждение было правильным, потому что казалось, что вы неправильно поняли то, что я пытался сказать, и пытались как-то исправить меня. - person Mike Marynowski; 17.03.2017
comment
Также с точки зрения других архитектур - если бы .NET был выпущен на них, у них были бы разные реализации Thread.Volatile методов, чтобы гарантировать соблюдение поведенческих гарантий. Я пытаюсь понять, почему работает текущая реализация, вот и все. Думаю, теперь я понял. Я думаю, что вы правы в отношении единственной проверки и зацикливания, поскольку я не могу придумать никаких примеров, которые работали бы без использования Interlocked для условной синхронизации единственного доступа, так что неважно :) - person Mike Marynowski; 17.03.2017
comment
похоже, что вы неправильно поняли то, что я пытался сказать, и пытались как-то меня поправить. - Пожалуйста, извините, я не собирался. если бы для них был выпущен .NET, у них были бы разные реализации Thread.Volatile методов, чтобы гарантировать соблюдение поведенческих гарантий. Я не знаю, есть .NET для ARM (например, Windows RT), и я считаю, что методы нетронуты, но у меня нет возможности проверить это сам прямо сейчас (например, запустить ILDasm или ILSpy на сборках этого фреймворка, mscorlib, System, System.Core и т. д.) - person acelent; 18.03.2017
comment
Я думаю, что большинство реализаций ARM также гарантируют согласованность кеша при записи, поэтому я не думаю, что это проблема, но поправьте меня, если я ошибаюсь. - person Mike Marynowski; 18.03.2017