Назначение ссылок атомарно, поэтому почему Interlocked.Exchange(ref Object, Object) необходимо?

В моей многопоточной веб-службе asmx у меня было поле класса _allData моего собственного типа SystemData, которое состоит из нескольких List<T> и Dictionary<T>, помеченных как volatile. Системные данные (_allData) обновляются раз в то время, и я делаю это, создавая еще один объект с именем newData и заполняя его структурами данных новыми данными. Когда это будет сделано, я просто назначу

private static volatile SystemData _allData

public static bool LoadAllSystemData()
{
    SystemData newData = new SystemData();
    /* fill newData with up-to-date data*/
     ...
    _allData = newData.
} 

Это должно работать, поскольку назначение является атомарным, и потоки, которые ссылаются на старые данные, продолжают использовать его, а остальные - новые системные данные сразу после назначения. Однако мой коллега сказал, что вместо того, чтобы использовать ключевое слово volatile и простое присваивание, я должен использовать InterLocked.Exchange, потому что он сказал, что на некоторых платформах он не гарантирует, что назначение ссылок является атомарным. Более того: когда я объявляю поле the _allData как volatile

Interlocked.Exchange<SystemData>(ref _allData, newData); 

выводит предупреждение "ссылка на нестабильное поле не будет считаться изменчивой". Что я должен думать об этом?

Ответ 1

Здесь есть множество вопросов. Рассматривая их по одному:

Назначение ссылок является атомарным, поэтому зачем нужен Interlocked.Exchange(ref Object, Object)?

Назначение ссылок является атомарным. Interlocked.Exchange не выполняет только задание ссылки. Он считывает текущее значение переменной, отбрасывает старое значение и назначает новое значение переменной, все как атомную операцию.

мой коллега сказал, что на некоторых платформах это не гарантирует, что ссылочное назначение является атомарным. Правильно ли мой коллега?

Нет. Назначение ссылок гарантировано будет атомарным на всех платформах .NET.

Мой коллега рассуждает о ложных предпосылках. Означает ли это, что их выводы неверны?

Не обязательно. Ваш коллега может дать вам хороший совет по плохим причинам. Возможно, есть и другая причина, почему вы должны использовать Interlocked.Exchange. Беззаботное программирование безумно сложно, и как только вы уйдете от хорошо зарекомендовавших себя практик, используемых экспертами в этой области, вы находитесь в сорняках и рискуете худшим видом условий гонки. Я не эксперт в этой области и не специалист по вашему коду, поэтому я не могу судить так или иначе.

выводит предупреждение "ссылка на нестабильное поле не будет считаться изменчивой". Что я должен думать об этом?

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

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

(Я предполагаю, что вы все это уже понимаете. Если у вас нет подробного понимания значения volatile и как он влияет на семантику кеша процессора, тогда вы не понимаете, как это работает и не следует использовать volatile. - бесплатные программы очень трудно получить, убедитесь, что ваша программа правильная, потому что вы понимаете, как это работает, а не случайно.)

Теперь предположим, что вы создаете переменную, которая является псевдонимом изменчивого поля, передавая ссылку на это поле. Внутри вызываемого метода у компилятора нет никакой причины знать, что ссылка должна иметь изменчивую семантику! Компилятор будет весело генерировать код для метода, который не реализует правила для изменчивых полей, но переменная является изменчивым полем. Это может полностью разрушить вашу логику без блокировки; предположение всегда состоит в том, что во временное поле всегда обращаются с изменчивой семантикой. Не имеет смысла относиться к нему как к летучим, а не к другим временам; вы должны всегда быть последовательными, иначе вы не можете гарантировать согласованность при других доступах.

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

Конечно, Interlocked.Exchange написан, чтобы ожидать изменчивого поля и делать правильные вещи. Предупреждение поэтому вводит в заблуждение. Я очень сожалею об этом; то, что мы должны были сделать, это реализовать некоторый механизм, в соответствии с которым автор метода, такого как Interlocked.Exchange, мог бы поместить атрибут в метод, говорящий "этот метод, который принимает ref, применяет изменчивую семантику к переменной, поэтому подавляйте предупреждение". Возможно, в будущей версии компилятора мы это сделаем.

Ответ 2

Либо ваш коллега ошибается, либо он знает что-то, что нет в спецификации языка С#.

5.5 Атоматичность ссылок переменных:

"Считывает и записывает следующие типы данных являются атомарными: bool, char, byte, sbyte, short, ushort, uint, int, float и reference."

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

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

Ответ 3

Interlocked.Exchange <T>

Устанавливает переменную указанного типа T в указанное значение и возвращает исходное значение как атомную операцию.

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

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

Ответ 4

Iterlocked.Exchange() не просто атомный, он также заботится о видимости памяти:

Следующие функции синхронизации используют соответствующие барьеры для обеспечения порядка памяти:

Функции, которые вводят или оставляют критические разделы

Функции, которые сигнализируют объекты синхронизации

Функции ожидания

Блокированные функции

Проблемы с синхронизацией и многопроцессором

Это означает, что помимо атомарности это гарантирует, что:

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