Состояние гонки на x86
Может ли кто-нибудь объяснить это утверждение:
shared variables
x = 0, y = 0
Core 1 Core 2
x = 1; y = 1;
r1 = y; r2 = x;
Как можно иметь r1 == 0
а также r2 == 0
на процессорах x86?
Источник "Язык параллелизма" Бартоша Милевского.
3 ответа
Проблема может возникнуть из-за оптимизаций, связанных с переупорядочением инструкций. Другими словами, оба процессора могут назначать r1
а также r2
перед назначением переменных x
а также y
, если они обнаружат, что это даст лучшую производительность. Эту проблему можно решить, добавив барьер памяти, который будет обеспечивать ограничение порядка.
Чтобы процитировать слайд-шоу, которое вы упомянули в своем посте:
Современные многоядерные / языки нарушают последовательную согласованность.
Что касается архитектуры x86, лучшим ресурсом для чтения является Руководство разработчика программного обеспечения для архитектуры Intel® 64 и IA-32 (глава 8.2. Упорядочение памяти). Разделы 8.2.1 и 8.2.2 описывают упорядочение памяти, реализованное процессорами Intel486, Pentium, Intel Core 2 Duo, Intel Atom, Intel Core Duo, Pentium 4, Intel Xeon и P6: модель памяти, называемая упорядочением процессора, как в отличие от программного упорядочения (строгого упорядочения) более старой архитектуры Intel386 (где инструкции чтения и записи всегда выполнялись в том порядке, в котором они появлялись в потоке команд).
В руководстве описываются многие гарантии упорядочения модели памяти упорядочения процессора (например, Нагрузки не переупорядочиваются с другими нагрузками, Хранилища не переупорядочиваются с другими хранилищами, Хранилища не переупорядочиваются со старыми нагрузками и т. Д.), Но также описывается разрешенное правило переупорядочения. что приводит к состоянию гонки в должности ОП:
8.2.3.4 Грузы могут быть переупорядочены с более ранними магазинами в разные места
С другой стороны, если первоначальный порядок инструкций был изменен:
shared variables
x = 0, y = 0
Core 1 Core 2
r1 = y; r2 = x;
x = 1; y = 1;
В этом случае процессор гарантирует, что r1 = 1
а также r2 = 1
Ситуация недопустима (из-за 8.2.3.3. Магазины не переупорядочиваются с гарантией более ранней загрузки), что означает, что эти инструкции никогда не будут переупорядочены в отдельных ядрах.
Чтобы сравнить это с различными архитектурами, ознакомьтесь с этой статьей: Упорядочение памяти в современных микропроцессорах ( это изображение специально). Вы можете видеть, что Itanium (IA-64) делает еще больше переупорядочения, чем архитектура IA-32.
На процессорах с более слабой моделью согласованности памяти (таких как SPARC, PowerPC, Itanium, ARM и т. Д.) Указанное выше условие может иметь место из-за отсутствия принудительной когерентности кэша при записи без явной инструкции барьера памяти. Так в основном Core1
видит запись на x
до y
, в то время как Core2
видит запись на y
до x
, В этом случае не требуется полная инструкция по ограничению... в основном вам нужно будет применять семантику записи или освобождения в этом сценарии, чтобы все записи фиксировались и были видимы для всех процессоров, прежде чем произойдут чтения по тем переменным, которые были написано Архитектуры процессоров с такими сильными моделями согласованности памяти, как x86, обычно делают это ненужным, но, как отмечает Гро, сам компилятор может переупорядочивать операции. Вы можете использовать volatile
ключевое слово в C и C++ для предотвращения переупорядочения операций компилятором в заданном потоке. Это не значит, что volatile
создаст потокобезопасный код, который управляет видимостью операций чтения и записи между потоками... для этого потребуется барьер памяти. Так что пока использование volatile
может по-прежнему создавать небезопасный многопоточный код, в данном потоке он будет обеспечивать последовательную согласованность на уровне машинного кода.
Вот почему некоторые говорят: темы, считающиеся вредными
Проблема заключается в том, что ни один из потоков не обеспечивает какого-либо упорядочения между двумя операторами, поскольку они не являются взаимозависимыми.
Компилятор знает, что x и y не являются псевдонимами, и поэтому не требуется упорядочивать операции.
Процессор знает, что x и y не являются псевдонимами, поэтому он может переупорядочить их по скорости. Хороший пример того, когда это происходит, - когда процессор обнаруживает возможность объединения записи. Он может объединить одну запись с другой, если он может сделать это, не нарушая свою модель когерентности.
Взаимозависимость выглядит странно, но на самом деле она ничем не отличается от любых других рас. Непосредственно писать код с использованием разделяемой памяти довольно сложно, и именно поэтому были разработаны параллельные языки и параллельные платформы для передачи сообщений, чтобы изолировать параллельные опасности для небольшого ядра и удалить опасности из самих приложений.