Подтвердить что ты не робот

Использование C/Pthreads: должны ли переменные переменные быть неустойчивыми?

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

Есть ли в PTHIX-стандарте pthread какие-либо слова об этом, зависит от этого компилятора или нет?

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

4b9b3361

Ответ 1

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

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

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

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

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

Ответ 3

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

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

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

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

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

Ответ 4

Существует широко распространенное мнение о том, что ключевое слово volatile полезно для многопоточного программирования.

Hans Boehm указывает, что для волатильности используются только три портативных устройства:

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

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

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

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

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

 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), будут переупорядочиваться. То, что вам действительно нужно для заказа, - это заборы памяти, также называемые барьерами памяти. Забор памяти предотвращает переупорядочение операций с памятью через забор или в некоторых случаях предотвращает переупорядочение в одном направлении. Многолетний не имеет ничего общего с заборами памяти.

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

  • Темы POSIX
  • Нити Windows (TM)
  • OpenMP
  • ТВВ

На основе статьи Arch Robison (Intel)

Ответ 5

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

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

Ответ 6

НЕТ.

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

Основное использование для Volatile - это доступ к I/O с отображением памяти. В этом случае базовое устройство может изменять значение места памяти независимо от CPU. Если вы не используете Volatile в этом состоянии, CPU может использовать ранее кэшированное значение памяти вместо того, чтобы читать обновленное значение.

Ответ 7

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

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

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

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

Ответ 8

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

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

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

Ответ 9

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

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

Это _must_not_ предполагается, что компилятор будет делать то, что вы хотите      с ссылками на память, которые не защищены READ_ONCE() и      WRITE_ONCE(). Без них компилятор находится в пределах своих прав на      делать всевозможные "творческие" преобразования, которые охватываются      раздел COMPILER BARRIER.

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

int y;
int x = READ_ONCE(y);

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

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

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

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


Как сказал Пол МакКенни:

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


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

Ответ 10

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

Volatile означает, что переменная обновляется вне области действия кода, и, таким образом, компилятор не может предположить, что он знает текущее значение. Даже барьеры памяти бесполезны, поскольку компилятор, который не обращает внимания на барьеры памяти (правда?), Все равно может использовать кешированное значение.

Ответ 11

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

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

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

Ответ 12

Нет.

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

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

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

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

Ответ 13

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