Если один поток записывает в какое-то место, а другой - читает, может ли второй поток увидеть новое значение, а не старое?

Начните с x = 0. Обратите внимание, что ни в одном из приведенных ниже кодов нет препятствий для памяти.

изменчивый int x = 0

Поток 1:

while (x == 0) {}
print "Saw non-zer0"
while (x != 0) {}
print "Saw zero again!"

Поток 2:

x = 1

Возможно ли когда-нибудь увидеть второе сообщение «Снова ноль!» На любом (реальном) процессоре? А как насчет x86_64?

Аналогично в этом коде:

изменчивый int x = 0.

Поток 1:

while (x == 0) {}
x = 2

Поток 2:

x = 1

Гарантированно ли окончательное значение x равно 2, или могут ли кэши ЦП обновлять основную память в произвольном порядке, так что хотя x = 1 попадает в кеш ЦП, где поток 1 может его видеть, тогда поток 1 перемещается в другой cpu, где он записывает x = 2 в кеш этого процессора, а x = 2 записывается обратно в основную память до x = 1.


person Martin C. Martin    schedule 02.10.2014    source источник
comment
Говоря в контексте языка C: поскольку это гонка данных, а гонка данных - это неопределенное поведение, то может случиться все, что угодно. На самом деле нет особого смысла пытаться это рассуждать.   -  person Michael Burr    schedule 02.10.2014
comment
Невозможно ответить без языка программирования (и будь то инструкции x86).   -  person usr    schedule 05.10.2014
comment
В большинстве языков компилятор может юридически переписать print(x); print(x); в old = x; print(x); print(old);. Так что на большинстве языков ответ положительный.   -  person usr    schedule 05.10.2014
comment
@MichaelBurr В контексте языка C есть только volatile операции с разделяемыми объектами, и они должны выполняться точно в соответствии с ABI; это часть наблюдаемого поведения. Таким образом, у нас есть в точности семантика, обеспечиваемая ЦП, когда читаются и записываются int объектов, соответствующих ABI (выровненных соответственно). Нет UB, только поведение ABI и CPU.   -  person curiousguy    schedule 28.05.2019
comment
@usr В C и C ++ volatile означает: выполнять эти операции с памятью в asm в точном порядке. И не оптимизируйте такую ​​операцию, даже если она может показаться излишней.   -  person curiousguy    schedule 28.05.2019


Ответы (3)


Да, это вполне возможно. Компилятор мог, например, только что записать x в память, но все еще иметь значение в регистре. Один while цикл может проверять память, а другой проверяет регистр.

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

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

person David Schwartz    schedule 02.10.2014
comment
Спасибо. Есть ли у вас какие-либо ссылки или примеры кода / сообщения в блогах, в которых это подробно обсуждается? Кроме того, я обновлю вопрос, чтобы использовать изменчивые переменные, чтобы обойти проблему с регистром; это больше из того, что я планировал изначально. - person Martin C. Martin; 02.10.2014

Оставляя в стороне уловки, выполняемые компилятором (даже те, которые разрешены языковыми стандартами), я полагаю, вы спрашиваете, как микроархитектура может вести себя в таком сценарии. Имейте в виду, что код, скорее всего, расширится до цикла ожидания занятости cmp [x] + jz или чего-то подобного, скрывающего внутри себя нагрузку. Это означает, что [x], скорее всего, будет находиться в кэше основного потока 1.

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

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

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

  Time -->                                        
  ----------------------------------------------------------------
thread 1

    cmp [x],0                                            execute
    je ...                                                  execute (not taken)
    ...
    cmp [x],0        execute
    jne ...              execute (not taken)
  Can_We_Get_Here:
    ...

thread2 

    store [x],1                          execute

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

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

8.2.3.2 Neither Loads Nor Stores Are Reordered with Like Operations

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

person Leeor    schedule 04.10.2014

Кажется, что ответ таков: «Это как раз и является задачей согласованности кеш-памяти ЦП». Процессоры x86 реализуют протокол MESI, который гарантирует, что второй поток не сможет увидеть новое значение тогда Старый.

person Martin C. Martin    schedule 06.10.2014
comment
Согласованность кэша сама по себе не гарантирует какой-либо модели упорядочения памяти. Протокол MESI не говорит, как будут упорядочены операции чтения / записи из двух разных контекстов, он только гарантирует, что независимо от порядка, в котором они будут формироваться, всегда будет считываться самое последнее значение (даже если оно записано другим ядром или сокетом). - person Leeor; 06.10.2014