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

StackOverflow https://stackoverflow.com/questions/78172

  •  09-06-2019
  •  | 
  •  

Вопрос

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

Имеет ли стандарт POSIX pthread какое-либо мнение по этому поводу, зависит ли это от компилятора или нет?

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

Это было полезно?

Решение

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

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

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

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

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

Другие советы

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

https://software.intel.com/en-us/blogs/2007/11/30/ Летучий-почти-useless-for-многопоточное-программирование/

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

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

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

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

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

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

Ганс Бём указывает на то что существует только три портативных варианта использования Volatility:

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

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

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

Давайте сначала разберемся с (1).Volatile не гарантирует атомарное чтение или запись.Например, энергозависимое чтение или запись 129-битной структуры не будет атомарной на большинстве современных аппаратных средств.Независимое чтение или запись 32-битного целого числа является атомарным на большинстве современных аппаратных средств, но изменчивость не имеет к этому никакого отношения.Вероятно, он был бы атомарным без летучих веществ.Атомарность зависит от прихоти компилятора.В стандартах C или 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™, поскольку компиляторы Itanium вставляют ограничения памяти для энергозависимых хранилищ.Это умное расширение Itanium.Но такие чипы, как Power™, будут переупорядочены.Что вам действительно нужно для заказа, так это заборы памяти, также называемый барьеры памяти.Ограждение памяти предотвращает переупорядочение операций с памятью через ограждение или, в некоторых случаях, предотвращает переупорядочение в одном направлении. Volatile не имеет ничего общего с ограждениями памяти.

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

  • POSIX-потоки
  • Потоки Windows(TM)
  • OpenMP
  • TBB

На основе статья Арча Робисона (Intel)

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

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

НЕТ.

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

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

Нет.

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top