Это сложный вопрос, пожалуйста, внимательно подумайте, прежде чем отвечать.
Рассмотрим эту ситуацию. Два потока (считывающее и записывающее) обращаются к одному глобальному int. Это безопасно? Обычно я бы ответил, не задумываясь, да!
Однако мне кажется, что Херб Саттер так не думает. В своих статьях об эффективном параллелизме он обсуждает некорректная очередь без блокировок и исправленная версия.
В конце первой статьи и в начале второй он обсуждает редко рассматриваемую особенность переменных - порядок записи. Int атомарны, хороши, но целые числа не обязательно упорядочены, что может разрушить любой алгоритм без блокировок, включая мой вышеописанный сценарий. Я полностью согласен с тем, что единственный способ исправить многопоточное поведение гарантия на всех существующих и будущих платформах - это использовать атомики (барьеры памяти AKA) или мьютексы.
Мой вопрос; когда-нибудь возникала проблема с перезагрузкой записи на реальном оборудовании? Или многопоточная паранойя просто педантична?
А как насчет классических однопроцессорных систем?
А как насчет более простых RISC-процессоров, таких как встроенный power-pc?
Разъяснение: Меня больше интересует, что г-н Саттер сказал об аппаратном (процессор / кеш) переупорядочении записи переменной. Я могу остановить оптимизатор от взлома кода с помощью переключателей компилятора или ручной проверки сборки после компиляции. Однако я хотел бы знать, может ли оборудование по-прежнему испортить код на практике.
Ага, здесь то же самое. «Обычно я бы ответил, не задумываясь, нет!». Все зависит от того, к какому оборудованию вы привыкли - на некоторых платформах вы можете обойтись просто «изменчивым», но не на других. Если вы привыкли писать переносимый код, вы всегда предполагаете «наихудшее» поведение.
... который в данном случае является многопроцессорным с некогерентными кешами памяти. Затем вы должны предположить, что изменения, сделанные в одном потоке, даже с изменчивым, будут распространяться никогда на другие потоки без барьера памяти. Эмпирическое правило: разные потоки работают на разных планетах и взаимодействуют только в принудительном порядке.
При чем тут сборка? Переупорядочивает не компилятор, а логика микрокода на самом ЦП. Конечно, компилятор может самостоятельно переупорядочить, но даже если вы обойдете это с помощью сборки, вы не выберетесь из леса.





Как вы сказали, из-за того, что переупорядочение выполняется на уровне кеша или процессора, вам действительно нужен какой-то барьер памяти для обеспечения правильной синхронизации, особенно для многопроцессорных систем (и особенно на платформах, отличных от x86). (Мне внушают, что однопроцессорные системы не имеют этих проблем, но не цитируйте меня по этому поводу - я определенно более склонен перестраховаться и в любом случае использовать синхронизированный доступ.)
Проблемы уже существуют даже в однопроцессорных системах. Например, ядра на базе PowerPC 60x прекрасно могут переупорядочивать операции ввода-вывода, поскольку в каждом ядре имеется несколько исполнительных модулей. Именно поэтому необходимы инструкции EIEIO, SYNC и ISYNC.
Да, даже если бы код работал на однопроцессорной машине, это была бы хрупкая конструкция кода, которая таинственным образом начинает давать сбои, когда вы переходите на многопроцессорную машину.
Или даже более умный однопроцессорный. С многопоточностью можно ожидать, что все, что явно не гарантировано, в какой-то момент в будущем загадочным образом изменится.
Да, я не понимаю, почему это должно быть ограничено многоядерными однопроцессорными машинами. Все дело в том, что на одноядерной однопроцессорной машине инструкции можно переупорядочивать на уровне микрокода.
Ага - используйте барьеры памяти, чтобы предотвратить переупорядочение инструкций там, где это необходимо. В некоторых компиляторах C++ ключевое слово volatile было расширено для вставки неявных барьеров памяти для каждого чтения и записи, но это не переносимое решение. (Аналогично API-интерфейсам Interlocked * win32). Vista даже добавляет несколько новых более тонких Interlocked API, которые позволяют указывать семантику чтения или записи.
К сожалению, C++ имеет настолько рыхлую модель памяти, что любой код, подобный этому, будет в некоторой степени непереносимым, и вам придется писать разные версии для разных платформ.
FWIW, C++ 0x представит переносимый механизм для написания поточно-ориентированного кода (вдохновленный библиотекой boost.thread).
Аллилуйя! Конечно, пройдет еще 5-10 лет, прежде чем код C++ 0x можно будет считать переносимым ...
Но решат ли возможности C++ 0x проблему модели памяти? Это не проблема потоковой передачи, поэтому я не вижу, что здесь могут предложить эти новые функции (или уже существующие в усилении). Это проблема с порядком инструкций.
Ага - C++ 0x представляет более четко определенную модель памяти, а также атомарные типы <>, которые можно явно изменять без использования блокировок. См. open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2427.html и open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2429.htm.
Ваша идея осмотра сборки недостаточно хороша; переупорядочивание может произойти на аппаратном уровне.
Чтобы ответить на ваш вопрос, "это когда-либо проблема на считывающем оборудовании": Да! На самом деле я сам сталкивался с этой проблемой.
Можно ли обойти проблему с однопроцессорными системами или в других особых случаях? Я бы сказал «нет», потому что через пять лет вам, возможно, все-таки придется работать на многоядерных процессорах, и тогда найти все эти места будет сложно (невозможно?).
Одно исключение: программное обеспечение, разработанное для встраиваемых аппаратных приложений, когда вы действительно полностью контролируете оборудование. На самом деле я "жульничал" вот так в таких ситуациях, например, процессор 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:
volatilein C#/.NET, as involatile int.volatileor * Atomic* in Java, as involatile int,AtomicInteger.atomic<T>in C++0x, the forthcoming ISO C++ Standard, as inatomic<int>.
Я не видел, как C++ 0x реализует упорядоченную атомарность, поэтому я не могу указать, является ли предстоящая языковая функция чистой реализацией библиотеки или также зависит от изменений языка. Вы можете просмотреть предложение, чтобы увидеть, можно ли его включить в качестве нестандартного расширения в вашу текущую цепочку инструментов, пока не будет доступен новый стандарт, он может быть уже доступен для вашей ситуации.
Мы столкнулись с проблемой, хотя и на процессорах Itanium, где переупорядочение инструкций более агрессивно, чем на x86 / x64.
Исправление заключалось в использовании инструкции Interlocked, поскольку (в то время) не было способа указать компилятору просто установить барьер записи после присвоения.
Нам действительно нужно расширение языка, чтобы решить эту проблему. Использование volatile (если поддерживается компилятором) является слишком грубым для случаев, когда вы пытаетесь выжать из фрагмента кода как можно больше производительности.
Ответ на вопрос «безопасно ли» по своей сути неоднозначен.
Это всегда безопасно, даже для двойников, в том смысле, что ваш компьютер не загорится. Это безопасно в том смысле, что вы всегда будете получать значение, которое int когда-то хранил в прошлом, Это небезопасно в том смысле, что вы можете получить значение, которое / будет обновлено другим потоком.
«Атомик» означает, что вы получаете вторую гарантию. Поскольку double обычно не является атомарным, вы можете получить 32 старых и 32 новых бита. Это явно небезопасно.
Когда я задал этот вопрос, меня больше всего интересовали однопроцессорные powerpc. В одном из комментариев InSciTek Джефф упомянул инструкции powerpc SYNC и ISYNC. Те, где ключ к однозначному ответу. Я нашел его здесь на сайте IBM.
Статья большая и довольно плотная, но вывод - нет, это небезопасно. На старых powerpc оптимизаторы памяти были недостаточно сложными, чтобы вызвать проблемы на однопроцессоре. Однако новые гораздо более агрессивны и могут нарушить даже простой доступ к глобальному int.
Нет, это небезопасно, и есть реальное доступное оборудование, которое демонстрирует эту проблему, например, модель памяти в микросхеме powerpc на xbox 360 позволяет переупорядочивать записи. Это усугубляется отсутствием барьеров во встроенных функциях, подробнее см. Эту статью о msdn.
Интересно. Моя первая реакция: «Если я не уверен, я бы не осмелился»