Использование C / Pthreads: общие переменные должны быть изменчивыми? - PullRequest
28 голосов
/ 17 сентября 2008

В языке программирования C и Pthreads в качестве библиотеки потоков; переменные / структуры, которые разделены между потоками, должны быть объявлены как volatile? Предполагая, что они могут быть защищены замком или нет (возможно, барьеры).

Говорит ли об этом стандарт Pthread POSIX, это зависит от компилятора или нет?

Изменить, чтобы добавить: Спасибо за отличные ответы. Но что, если вы не используете блокировки; что если вы используете барьеры например? Или код, который использует примитивы, такие как compare-and-swap для прямого и атомарного изменения общей переменной ...

Ответы [ 13 ]

26 голосов
/ 17 сентября 2008

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

https://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/

9 голосов
/ 24 апреля 2009

Ответ абсолютно, однозначно, НЕТ. Вам не нужно использовать 'volatile' в дополнение к правильным примитивам синхронизации. Все, что нужно сделать, сделано этими примитивами.

Использование 'volatile' не является ни необходимым, ни достаточным. Это не обязательно, потому что правильных примитивов синхронизации достаточно. Этого недостаточно, потому что он отключает только некоторые оптимизации, а не все, которые могут вас укусить. Например, он не гарантирует атомарность или видимость на другом процессоре.

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

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

С другой стороны, если вы используете примитивы синхронизации с определенной многопоточной семантикой, вам гарантировано, что все будет работать. В качестве плюса, вы не воспринимаете огромный удар по производительности как «volatile». Так почему бы не сделать это таким образом?

6 голосов
/ 03 октября 2008

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

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

Особенно на машинах с большим количеством регистров (т. Е. Не в x86) переменные могут довольно долго жить в регистрах, а хороший компилятор может кэшировать даже части структур или целые структуры в регистрах. Таким образом, вы должны использовать volatile, но для производительности, также копировать значения в локальные переменные для вычисления, а затем делать явную обратную запись. По сути, использование volatile эффективно означает, что нужно немного подумать о загрузке хранилища в своем C-коде.

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

Пример слабости volatile приведен в примере алгоритма моего Деккера по адресу http://jakob.engbloms.se/archives/65,, который довольно хорошо доказывает, что volatile не работает для синхронизации.

3 голосов
/ 14 ноября 2011

Широко распространено мнение, что ключевое слово volatile хорошо для многопоточного программирования.

Ханс Боэм указывает , что существует только три портативных варианта использования летучих:

  • volatile может использоваться для маркировки локальных переменных в той же области видимости, что и setjmp, значение которого должно сохраняться в longjmp. Неясно, какая доля таких видов использования будет замедлена, поскольку ограничения атомарности и порядка не действуют, если нет возможности совместно использовать данную локальную переменную. (Даже неясно, какая доля такого использования будет замедлена, если все переменные будут сохранены в длинном диапазоне, но это отдельный вопрос, который здесь не рассматривается.)
  • volatile может использоваться, когда переменные могут быть «внешне модифицированы», но изменение фактически инициируется синхронно самим потоком, например, потому что основная память отображается в нескольких местах.
  • A volatile sigatomic_t может использоваться для связи с обработчиком сигнала в том же потоке ограниченным образом. Можно подумать об ослаблении требований к случаю sigatomic_t, но это выглядит довольно нелогичным.

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

  • валентность
  • согласованность памяти , то есть порядок операций потока, видимый другим потоком.

Давайте сначала разберемся с (1). Volatile не гарантирует атомарного чтения или записи. Например, энергозависимое чтение или запись 129-битной структуры не будет атомарным на большинстве современных аппаратных средств. Изменчивое чтение или запись 32-битного int является атомарным на большинстве современного оборудования, но volatile не имеет к этому никакого отношения . Скорее всего, он будет атомным без летучих. Атомность находится в прихоти компилятора. В стандартах C или C ++ нет ничего, что говорило бы, что оно должно быть атомарным.

Теперь рассмотрим вопрос (2). Иногда программисты думают о volatile как об отключении оптимизации энергозависимого доступа. Это в значительной степени верно на практике. Но это только волатильные доступы, а не энергонезависимые. Рассмотрим этот фрагмент:

 volatile int Ready;       

    int Message[100];      

    void foo( int i ) {      

        Message[i/10] = 42;      

        Ready = 1;      

    }

Он пытается сделать что-то очень разумное в многопоточном программировании: написать сообщение, а затем отправить его в другой поток. Другой поток будет ждать, пока Ready не станет ненулевым, и затем прочитает сообщение. Попробуйте скомпилировать это с помощью "gcc -O2 -S", используя gcc 4.0 или icc. Оба сначала сохранят данные в Ready, так что они могут перекрываться с вычислением i / 10. Изменение порядка не является ошибкой компилятора. Это агрессивный оптимизатор, выполняющий свою работу.

Вы можете подумать, что решение состоит в том, чтобы пометить все ссылки на память как изменчивые. Это просто глупо. Как говорят предыдущие цитаты, это просто замедлит ваш код. Что еще хуже, это может не решить проблему. Даже если компилятор не переупорядочивает ссылки, аппаратное обеспечение может. В этом примере аппаратное обеспечение x86 не будет переупорядочивать его. Так же как и процессор Itanium (TM), поскольку компиляторы Itanium вставляют ограждения памяти для энергозависимых хранилищ. Это умное расширение Itanium. Но чипы вроде Power (TM) будут переупорядочены. Для заказа вам действительно понадобятся ограждения памяти , также называемые барьеры памяти . Ограничитель памяти предотвращает изменение порядка операций с памятью через ограничитель или, в некоторых случаях, предотвращает изменение порядка следования в одном направлении. Volatile не имеет ничего общего с ограничителями памяти.

Так, каково решение для многопоточного программирования? Используйте расширение библиотеки или языка, которое реализует атомарную семантику и семантику заборов. При использовании по назначению, операции в библиотеке вставят правильные заборы. Некоторые примеры:

  • POSIX темы
  • Windows (TM) темы
  • OpenMP
  • TBB

Основано на статье Арка Робисона (Intel)

2 голосов
/ 18 февраля 2009

NO.

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

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

2 голосов
/ 17 сентября 2008

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

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

0 голосов
/ 28 октября 2017

Основная причина заключается в том, что семантика языка C основана на однопоточной абстрактной машине . И компилятор имеет собственное право на преобразование программы, пока «наблюдаемое поведение» программы на абстрактной машине остается неизменным. Он может объединять смежные или перекрывающиеся обращения к памяти, повторять доступ к памяти несколько раз (например, при проливе регистра) или просто отбрасывать доступ к памяти, если он думает о поведении программы, когда выполняется в единственном потоке , не меняется Поэтому, как вы можете подозревать, поведение do меняется, если программа действительно должна выполняться многопоточным образом.

Как отметил Пол Маккенни в знаменитом документе ядра Linux :

_must_not_ предполагается, что компилятор будет делать то, что вы хотите со ссылками на память, которые не защищены READ_ONCE () и WRITE_ONCE (). Без них компилятор имеет право на делать всевозможные «творческие» преобразования, которые рассматриваются в раздел БАРЬЕР КОМПИЛЕРА.

READ_ONCE () и WRITE_ONCE () определены как изменчивые приведения к указанным переменным. Таким образом:

int y;
int x = READ_ONCE(y);

эквивалентно:

int y;
int x = *(volatile int *)&y;

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

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


Как отметил Пол МакКенни:

Я видел блеск в их глазах, когда они обсуждали методы оптимизации, о которых вы не хотели бы, чтобы ваши дети знали!


Но посмотрите, что происходит с C11 / C ++ 11 .

0 голосов
/ 13 апреля 2016

номер

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

Во-вторых, volatile недостаточно. Стандарт C не предоставляет никаких гарантий многопоточного поведения для переменных, объявленных volatile.

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

Единственным исключением могут быть конкретные платформы (такие как Visual Studio), где она имеет документированную многопоточную семантику.

0 голосов
/ 02 ноября 2010

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

0 голосов
/ 22 сентября 2010

Некоторые люди, очевидно, предполагают, что компилятор рассматривает вызовы синхронизации как барьеры памяти. «Кейси» предполагает, что есть только один процессор.

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

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

...