Многопоточная паранойя

Это сложный вопрос, пожалуйста, внимательно подумайте, прежде чем отвечать.

Рассмотрим эту ситуацию. Два потока (считывающее и записывающее) обращаются к одному глобальному int. Это безопасно? Обычно я бы ответил, не задумываясь, да!

Однако мне кажется, что Херб Саттер так не думает. В своих статьях об эффективном параллелизме он обсуждает некорректная очередь без блокировок и исправленная версия.

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

Мой вопрос; когда-нибудь возникала проблема с перезагрузкой записи на реальном оборудовании? Или многопоточная паранойя просто педантична? А как насчет классических однопроцессорных систем?
А как насчет более простых RISC-процессоров, таких как встроенный power-pc?

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

Интересно. Моя первая реакция: «Если я не уверен, я бы не осмелился»

svrist 03.01.2009 22:49

Ага, здесь то же самое. «Обычно я бы ответил, не задумываясь, нет!». Все зависит от того, к какому оборудованию вы привыкли - на некоторых платформах вы можете обойтись просто «изменчивым», но не на других. Если вы привыкли писать переносимый код, вы всегда предполагаете «наихудшее» поведение.

Steve Jessop 04.01.2009 16:42

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

Steve Jessop 04.01.2009 16:46

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

ApplePieIsGood 05.01.2009 17:58
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
29
4
2 022
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

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

Проблемы уже существуют даже в однопроцессорных системах. Например, ядра на базе PowerPC 60x прекрасно могут переупорядочивать операции ввода-вывода, поскольку в каждом ядре имеется несколько исполнительных модулей. Именно поэтому необходимы инструкции EIEIO, SYNC и ISYNC.

Tall Jeff 03.01.2009 23:04

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

Bill Karwin 03.01.2009 23:21

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

Eclipse 03.01.2009 23:22

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

ApplePieIsGood 05.01.2009 18:03

Ага - используйте барьеры памяти, чтобы предотвратить переупорядочение инструкций там, где это необходимо. В некоторых компиляторах C++ ключевое слово volatile было расширено для вставки неявных барьеров памяти для каждого чтения и записи, но это не переносимое решение. (Аналогично API-интерфейсам Interlocked * win32). Vista даже добавляет несколько новых более тонких Interlocked API, которые позволяют указывать семантику чтения или записи.

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

FWIW, C++ 0x представит переносимый механизм для написания поточно-ориентированного кода (вдохновленный библиотекой boost.thread).

Shog9 03.01.2009 23:07

Аллилуйя! Конечно, пройдет еще 5-10 лет, прежде чем код C++ 0x можно будет считать переносимым ...

Eclipse 03.01.2009 23:20

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

ApplePieIsGood 05.01.2009 18:01

Ага - C++ 0x представляет более четко определенную модель памяти, а также атомарные типы <>, которые можно явно изменять без использования блокировок. См. open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2427.html и open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2429.htm.

Eclipse 05.01.2009 20:16
Ответ принят как подходящий

Ваша идея осмотра сборки недостаточно хороша; переупорядочивание может произойти на аппаратном уровне.

Чтобы ответить на ваш вопрос, "это когда-либо проблема на считывающем оборудовании": Да! На самом деле я сам сталкивался с этой проблемой.

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

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

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

Если вы хотите увидеть, насколько все может быть плохо, поищите научные статьи по модели памяти Java (а теперь и по модели памяти C++). Учитывая переупорядочивание, которое может сделать реальное оборудование, попытка выяснить, что безопасно на языке высокого уровня, является кошмаром.

is this ever a problem on real hardware?

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

Цитата из упомянутой статьи Херба Саттера (вторая)

Ordered atomic variables are spelled in different ways on popular platforms and environments. For example:

  • volatile in C#/.NET, as in volatile int.
  • volatile or * Atomic* in Java, as in volatile int, AtomicInteger.
  • atomic<T> in C++0x, the forthcoming ISO C++ Standard, as in atomic<int>.

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

Мы столкнулись с проблемой, хотя и на процессорах Itanium, где переупорядочение инструкций более агрессивно, чем на x86 / x64.

Исправление заключалось в использовании инструкции Interlocked, поскольку (в то время) не было способа указать компилятору просто установить барьер записи после присвоения.

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

Ответ на вопрос «безопасно ли» по своей сути неоднозначен.

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

«Атомик» означает, что вы получаете вторую гарантию. Поскольку double обычно не является атомарным, вы можете получить 32 старых и 32 новых бита. Это явно небезопасно.

Когда я задал этот вопрос, меня больше всего интересовали однопроцессорные powerpc. В одном из комментариев InSciTek Джефф упомянул инструкции powerpc SYNC и ISYNC. Те, где ключ к однозначному ответу. Я нашел его здесь на сайте IBM.

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

Нет, это небезопасно, и есть реальное доступное оборудование, которое демонстрирует эту проблему, например, модель памяти в микросхеме powerpc на xbox 360 позволяет переупорядочивать записи. Это усугубляется отсутствием барьеров во встроенных функциях, подробнее см. Эту статью о msdn.

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