Достаточно ли функций блокировки мьютекса без энергозависимости?

Мы с коллегой пишем программное обеспечение для различных платформ, работающих на x86, x64, Itanium, PowerPC и других серверных процессорах 10-летней давности.

Мы только что обсудили, достаточно ли функций мьютекса, таких как pthread_mutex_lock () ... pthread_mutex_unlock (), или же защищенная переменная должна быть изменчивой.

int foo::bar()
{
 //...
 //code which may or may not access _protected.
 pthread_mutex_lock(m);
 int ret = _protected;
 pthread_mutex_unlock(m);
 return ret;
}

Меня беспокоит кеширование. Может ли компилятор разместить копию _protected в стеке или в регистре и использовать это устаревшее значение в назначении? Если нет, то что этому мешает? Уязвимы ли вариации этого паттерна?

Я предполагаю, что компилятор на самом деле не понимает, что pthread_mutex_lock () - это специальная функция, так что мы просто защищены точками последовательности?

Большое спасибо.

Обновление: хорошо, я вижу тенденцию с ответами, объясняющими, почему волатильность - это плохо. Я уважаю эти ответы, но статьи на эту тему легко найти в Интернете. То, что я не могу найти в Интернете, и причина, по которой я задаю этот вопрос, заключается в том, как я защищен без изменчивости. Если приведенный выше код правильный, насколько он неуязвим для проблем с кешированием?


person David    schedule 26.07.2011    source источник
comment
возможный дубликат Использование C / Pthreads: нужны ли общие переменные быть нестабильным?   -  person Emile Cormier    schedule 27.07.2011
comment
См. Также: stackoverflow.com/questions/3208060/   -  person Emile Cormier    schedule 27.07.2011


Ответы (7)


Если приведенный выше код правильный, как он неуязвим для проблем с кешированием?

До C ++ 0x это не так. И это не указано в C. Итак, это действительно зависит от компилятора. В общем, если компилятор не гарантирует, что он будет соблюдать ограничения порядка доступа к памяти для функций или операций, которые включают несколько потоков, вы не сможете писать многопоточный безопасный код с помощью этого компилятора. См. Потоки не могут быть реализованы как библиотеки Ганса Дж. Боэма.

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

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

person MSN    schedule 26.07.2011
comment
Интересный. Я уверен, что многие платформы, которые мы поддерживаем, умрут, прежде чем получат C ++ 0x, но этого стоит ожидать через 5-10 лет. Я прочитаю этот документ, спасибо. - person David; 27.07.2011
comment
Эта статья Боэма великолепна - и это отличный взрыв из прошлого: многопроцессоры, наконец, становятся мейнстримом. Ух ты - 2004 год не скучаю! - person sage; 05.03.2017
comment
Не могли бы вы объяснить, как компилятор может некорректно компилировать код, представленный в Q? - person curiousguy; 06.11.2019
comment
До C ++ 0x это не так Этот ответ неверен. pthread_mutex_lock() и pthread_mutex_unlock() - это функции POSIX, которые гарантируют надлежащее поддержание порядка доступа к памяти : - person Andrew Henle; 23.11.2019
comment
(продолжение) Приложения должны гарантировать, что доступ к любой ячейке памяти более чем одним потоком управления (потоками или процессами) ограничен таким образом, чтобы ни один поток управления не мог читать или изменять ячейку памяти, в то время как другой поток управления может ее изменять. Такой доступ ограничен с помощью функций, которые синхронизируют выполнение потока, а также синхронизируют память по отношению к другим потокам. Следующие функции синхронизируют память с другими потоками: ... pthread_mutex_lock() ... pthread_mutex_unlock() ... - person Andrew Henle; 23.11.2019
comment
@AndrewHenle: Действительно, обычный механизм состоит в том, чтобы они не были встроенными (непрозрачными) функциями, поэтому оптимизатор должен предполагать, что они изменяют любые глобально доступные переменные, такие как _protected. Как функции блокировки и разблокировки мьютекса предотвращают переупорядочение ЦП?. Например, они не препятствуют тому, чтобы локальные переменные, такие как счетчики циклов, оставались в регистрах, если их адрес (потенциально) не экранировал функцию. Для правильности это имеет тот же эффект, что и реальное поведение: другие потоки могли изменить любую общую переменную. - person Peter Cordes; 15.07.2021

Самый простой ответ - volatile вообще не нужен для многопоточности.

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

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

Единственное, для чего volatile следует использовать в C ++, - это разрешить доступ к устройствам с отображением памяти, разрешить использование переменных между setjmp и longjmp и разрешить использование sig_atomic_t переменных в обработчиках сигналов. Само ключевое слово не делает переменную атомарной.

Хорошие новости: в C ++ 0x у нас будет STL-конструкция std::atomic, которую можно использовать для обеспечения атомарных операций и поточно-ориентированных конструкций для переменных. Пока ваш любимый компилятор не поддерживает это, вам может потребоваться обратиться к библиотеке boost или вырезать некоторый ассемблерный код, чтобы создать свои собственные объекты для предоставления атомарных переменных.

P.S. Большая путаница вызвана тем, что Java и .NET фактически применяют многопоточную семантику с помощью ключевого слова volatile. C ++, однако следует тому же примеру с C, где это не так.

person AJG85    schedule 26.07.2011
comment
Спасибо, я читал это несколько раз в Интернете. Однако я действительно хочу понять, как я защищаюсь без летучести. - person David; 27.07.2011
comment
Я сам этого не понимал, пока не прочитал следующее: aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf < / а> - person AJG85; 27.07.2011

Ваша библиотека потоковой передачи должна включать соответствующие барьеры ЦП и компилятора для блокировки и разблокировки мьютексов. Для GCC memory clobber в инструкции asm действует как барьер компилятора.

На самом деле, есть две вещи, которые защищают ваш код от кеширования (компилятора):

  • Вы вызываете нечистую внешнюю функцию (pthread_mutex_*()), что означает, что компилятор не знает, что эта функция не изменяет ваши глобальные переменные, поэтому он должен их перезагрузить.
  • Как я уже сказал, pthread_mutex_*() включает барьер компилятора, например: в glibc / x86 pthread_mutex_lock() заканчивается вызовом макроса _ 5_, в котором есть memory clobber, заставляющий компилятор перезагружать переменные.
person ninjalj    schedule 26.07.2011
comment
Ваша первая точка зрения является частью спецификации C? Или это зависит от компилятора? - person Samuel; 30.08.2018
comment
@Samuel: зависит от набора инструментов. До C11 (выпущенного в декабре 2011 г., IIRC) в стандарте не было понятия потоков. - person ninjalj; 01.09.2018
comment
@Samuel Или это зависит от компилятора? Точка, в которой вызов функции с неизвестным телом делает неизвестные вещи? Это зависит от знаний. Если у вас есть способность читать мысли, вы можете попытаться узнать, нужен ли внешней функции доступ к переменной, в противном случае вы просто предполагаете, что это так. - person curiousguy; 06.11.2019
comment
Обычный механизм заключается в том, чтобы просто избежать предоставления встроенных определений pthread_mutex_*, поэтому это непрозрачный вызов функции, который оптимизатор должен предполагать, считывает и записывает любую / всю глобально доступную память. Как функции блокировки и разблокировки мьютекса предотвращают переупорядочение ЦП?. (Точно такое же поведение, что и asm("" ::: "memory").) Итак, да, компилятор не может сохранять значения shared-var в регистрах во время вызова. (Мне не нравится называть это кешированием, поскольку это приводит к путанице с кешами ЦП и согласованностью, но такое использование технически не является неправильным) - person Peter Cordes; 15.07.2021

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

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

person jbruni    schedule 26.07.2011

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

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

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

Я предполагаю, что компилятор на самом деле не понимает, что pthread_mutex_lock () - это специальная функция, так что мы просто защищены точками последовательности?

Компилятор не знает, что делает, поэтому не пытается оптимизировать его.

Как это «особенное»? Он непрозрачен и рассматривается как таковой. Он не особенный среди непрозрачных функций.

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

Меня беспокоит кеширование. Может ли компилятор разместить копию _protected в стеке или в регистре и использовать это устаревшее значение в назначении?

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

Так что да, между вызовами непрозрачных функций. Не поперек.

А также для переменных, которые могут использоваться только в функции, по имени: для локальных переменных, у которых нет ни адреса, ни ссылки на них (так что компилятор не может выполнить все дальнейшие действия) использует). Их действительно можно «кэшировать» по произвольным вызовам, включая блокировку / разблокировку.

Если нет, то что этому мешает? Уязвимы ли вариации этого паттерна?

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

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

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

  • функция является «встроенной» (= доступна для встраивания) (либо включается глобальная оптимизация, либо все функции морально «встроены»);
  • никакого барьера памяти не требуется (как в однопроцессорной системе с разделением времени, так и в многопроцессорной строго упорядоченной системе) в этом примитиве синхронизации (блокировка или разблокировка), поэтому он не содержит такой вещи;
  • не используется никаких специальных инструкций (например, сравнения и установки) (например, для блокировки спина операция разблокировки представляет собой простую запись);
  • отсутствует системный вызов для приостановки или пробуждения потоков (не требуется при блокировке вращения);

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

или должна ли защищенная переменная быть изменчивой.

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

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

  • реальный ввод-вывод, такой как iostream
  • системные вызовы
  • другие нестабильные операции
  • asm memory clobbers (но тогда побочный эффект памяти не переупорядочивается вокруг них)
  • вызовы внешних функций (как они могли бы сделать одно из указанных выше)

Энергозависимость не упорядочена с учетом побочных эффектов энергонезависимой памяти. Это делает энергозависимую практически бесполезной (бесполезной для практического использования) для написания поточно-безопасного кода даже в самом конкретном случае, когда volatile может a priori помочь в том случае, когда не требуется никакого ограждения памяти: при программировании потоковых примитивов в системе разделения времени на одном процессоре. (Это может быть одним из наименее понятых аспектов C или C ++.)

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

person curiousguy    schedule 22.11.2019

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

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

Пример: см. Пример одноэлементного шаблона
https://en.m.wikipedia.org/wiki/Singleton_pattern#Lazy_initialization

Почему кто-то пишет такой код? Ответ: Отсутствие накопления блокировки дает преимущество в производительности.

PS: Это мой первый пост о переполнении стека.

person Gangadhar Mylapuram    schedule 14.07.2021
comment
Кэш ЦП согласован; замки не нужно смывать. Данных, находящихся в кеше ЦП, достаточно для того, чтобы они были видны во всем мире. Не путайте компиляторы, которые предпочитают хранить значения переменных в регистрах, с тем, как оборудование кэширует память. Регистры являются частными для каждого ядра, ЦП-кеш согласован. См. Также software.rajivprab.com/2018 / 04/29 / - person Peter Cordes; 15.07.2021
comment
Кажется, вы предлагаете использовать volatile для бесконтактной атомики. (Когда использовать volatile с многопоточностью?) Это плохая идея; C ++ 11 std::atomic исполнилось десять лет; используйте его вместо этого. (С std::memory_order_relaxed, если вы хотите, чтобы он скомпилировался примерно до того же asm, что и с volatile, или memory_order_acquire, вероятно, необходим как минимум для второго шага двойной проверки. И, кстати, компиляторы, такие как GCC, создают asm, который использует двойной -проверьте дешевым только для чтения сначала проверьте локальные static переменные функции с неконстантными инициализаторами.) - person Peter Cordes; 15.07.2021

Нет, если объект, который вы блокируете, является изменчивым, например: если значение, которое он представляет, зависит от чего-то постороннего для программы (состояние оборудования). volatile НЕ следует использовать для обозначения какого-либо поведения, являющегося результатом выполнения программы. Если это действительно volatile, то я бы лично заблокировал значение указателя / адреса вместо базового объекта. например:

volatile int i = 0;
// ... Later in a thread
// ... Code that may not access anything without a lock
std::uintptr_t ptr_to_lock = &i;
some_lock(ptr_to_lock);
// use i
release_some_lock(ptr_to_lock);

Обратите внимание, что это работает, только если ВСЕ код, когда-либо использующий объект в потоке, блокирует один и тот же адрес. Так что помните об этом при использовании потоков с некоторой переменной, которая является частью API.

person ViralTaco_    schedule 16.07.2021