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

108

В моей многопоточной веб-службе 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); 

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

char m
источник

Ответы:

180

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Эрик Липперт
источник
1
Из того, что я слышал, Interlocked.Exchange также гарантирует создание барьера памяти. Итак, если вы, например, создаете новый объект, затем назначаете пару свойств, а затем сохраняете объект в другой ссылке без использования Interlocked.Exchange, тогда компилятор может испортить порядок этих операций, что сделает доступ ко второй ссылке не потоком- сейф. Так ли это на самом деле? Есть ли смысл использовать Interlocked.Exchange в подобных сценариях?
Майк,
12
@Mike: Когда дело доходит до того, что, возможно, наблюдается в многопоточных ситуациях с низкой блокировкой, я невежественен, как и следующий парень. Ответ, вероятно, будет отличаться от процессора к процессору. Вы должны задать свой вопрос эксперту или почитать по теме, если она вас интересует. Книга Джо Даффи и его блог - хорошее место для начала. Мое правило: не используйте многопоточность. Если необходимо, используйте неизменяемые структуры данных. Если не можете, используйте замки. Только тогда, когда у вас должны быть изменяемые данные без блокировок, вам следует рассматривать методы с низким уровнем блокировки.
Эрик Липперт,
Спасибо за ответ, Эрик. Это действительно меня интересует, поэтому я читал книги и блоги о стратегиях многопоточности и блокировки, а также пытался реализовать их в своем коде. Но предстоит еще многому научиться ...
Майк,
2
@EricLippert Между фразами «не используйте многопоточность» и «если необходимо, используйте неизменяемые структуры данных», я бы вставил промежуточный и очень распространенный уровень «иметь дочерний поток, использующий только объекты ввода, принадлежащие исключительно владельцу, а родительский поток потребляет результаты. только когда ребенок кончит ». Как в var myresult = await Task.Factory.CreateNew(() => MyWork(exclusivelyLocalStuffOrValueTypeOrCopy));.
Джон
1
@ Джон: Это хорошая идея. Я стараюсь относиться к потокам как к дешевым процессам: они нужны для выполнения работы и получения результата, а не для того, чтобы быть вторым потоком управления внутри структур данных основной программы. Но если объем работы, выполняемой потоком, настолько велик, что разумно рассматривать его как процесс, тогда я говорю, просто сделайте это процессом!
Эрик Липперт
9

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

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

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

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

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

Гуффа
источник
3
@guffa: да, я тоже это читал. остается исходный вопрос: «присвоение ссылки является атомарным, так зачем нужен Interlocked.Exchange (ref Object, Object)?» unaswered
голец м
@zebrabox: что ты имеешь в виду? когда их нет? что бы вы сделали?
char m
@matti: это необходимо, когда вам нужно прочитать и записать значение как атомарную операцию.
Guffa
Как часто на самом деле вам приходится беспокоиться о том, что память не выравнивается правильно в .NET? Взаимодействие с тяжелыми вещами?
Skurmedel 03
1
@zebrabox: Спецификация не перечисляет это предостережение, она дает очень четкое заявление. У вас есть ссылка на ситуацию, не связанную с памятью, когда чтение или запись ссылки не могут быть атомарными? Похоже, это нарушит очень четкий язык спецификации.
TJ Crowder
6

Блокировано. Обмен <T>

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

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

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

Гийом
источник
3

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

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

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

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

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

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

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

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

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