Гарантируют ли изменчивые переменные в C / C ++ последовательной семантикой между потоками?

Есть ли какая-либо гарантия со стороны какого-либо общепринятого стандарта (ISO C или C ++ или любой из спецификаций POSIX / SUS), что переменная (возможно, помеченная как изменчивая), не защищенная мьютексом, к которой осуществляется доступ несколькими потоками, в конечном итоге станет согласованной если это назначено?

Чтобы предоставить конкретный пример, рассмотрим два потока, совместно использующих переменную v с начальным значением ноль.

Поток 1: v = 1

Поток 2: while (v == 0) yield ();

Гарантированно ли завершение потока 2 в конечном итоге? Или он может вращаться вечно, потому что когерентность кеша никогда не срабатывает и делает назначение видимым в кэше потока 2?

Я знаю, что стандарты C и C ++ (до C ++ 0x) вообще не говорят о потоках или параллелизме. Но мне любопытно, гарантирует ли это модель памяти C ++ 0x, pthreads или что-то еще. (По-видимому, это действительно работает в Windows на 32-битной x86; мне интересно, на что можно положиться в целом, или это просто работает там).


person Jack Lloyd    schedule 24.06.2010    source источник
comment
Когерентность кэша реализована на процессоре, и он всегда срабатывает (по крайней мере, на основных архитектурах). Это не то, о чем программа что-либо говорит. Если что-то записывается в кеш, это записывается в память, и все другие потоки это увидят. Это не проблема многопоточности. Проблема в том, происходит ли запись в память и происходит ли это в ожидаемое время   -  person jalf    schedule 25.06.2010
comment
Он будет работать на архитектурах Intel. До меня доходили слухи об архитектурах, на которых это не работает, но я никогда не видел их лично.   -  person Omnifarious    schedule 02.10.2011
comment
ARM (например) разработали многоядерные архитектуры, которые не имеют согласованного кеша. Не уверен, сколько на самом деле используются эти конструкции. Преимущество состоит в том, что вы экономите некоторое количество кремния и тепла, которое используется для синхронизации всего, но, конечно, недостатком является то, что это сбивает с толку людей, привыкших к модели потоковой передачи Intel.   -  person Steve Jessop    schedule 03.11.2011


Ответы (6)


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

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

РЕДАКТИРОВАТЬ: всем, кто все еще не понимает ключевого слова volatile - volatile гарантирует, что компилятор не будет генерировать код, который явно кэширует данные в регистрах, но это НЕ то же самое, что иметь дело с оборудованием, которое прозрачно кэширует / переупорядочивает читает и пишет. Прочтите, например, это или this, или эта статья доктора Доббса или ответ на этот SO вопрос, или просто выберите свой любимый компилятор, нацеленный на слабо согласованную архитектуру памяти, такую ​​как Cell, напишите тестовый код и сравните, что компилятор генерирует то, что вам нужно, чтобы гарантировать, что записи будут видны другим процессам.

person moonshadow    schedule 24.06.2010
comment
Компилятор должен делать то, что ему подходит, чтобы гарантировать, что все записи в переменные volatile действительно попадут в основную память, а это, в свою очередь, сделает ее видимой для других потоков. - person David Rodríguez - dribeas; 25.06.2010
comment
@ Дэвид: это ошибка. Доступ к volatile объектам должен оцениваться строго в соответствии с абстрактной машиной, определенной стандартом языка. Это заявление о том, какие оптимизации может выполнять C ++, а не о том, какую дополнительную обработку программист может пожелать сделать для устранения архитектурных причуд. Он говорит, что компилятор должен сгенерировать явную инструкцию записи для каждого присваивания в источнике, но он ничего не говорит о генерации flush, sync или eieio или того, что может потребоваться вашему процессору, чтобы фактически вызвать попадание данных в память в программном порядке или в все. - person moonshadow; 25.06.2010
comment
Есть еще утверждения о volatile. Важнейшим из них является то, что их чтение и запись являются наблюдаемыми побочными эффектами. В частности, цикл из вопроса должен многократно читать v. Это может не кэшировать значение. Ни в регистре, ни в кэше L1, ни где-либо еще. - person MSalters; 25.06.2010
comment
@MSalters: ваш вывод, опять же, неверен. Чтение и запись в volatiles - наблюдаемые побочные эффекты: опять же, это утверждение о том, какой вид оптимизации компилятор может не выполнять, а не утверждение о дополнительном коде, который он должен генерировать. Компилятор может не генерировать код, который кэширует изменчивые данные, но за аппаратное кэширование данных, которые ему было сказано хранить, компилятор не несет ответственности. - person moonshadow; 25.06.2010
comment
Верно, что обычно вам не нужно ничего делать, чтобы сделать состояние видимым для других потоков. Но обычно вы действительно заботитесь о согласованности (обычно вы выполняете эту синхронизацию, чтобы гарантировать, что определенный код выполняется только в определенное время, в определенном состоянии), и тогда код OP не может больше на него можно положиться. - person jalf; 25.06.2010
comment
@moonshadow: вы искусственно различаете генерацию кода и оптимизацию. Компилятор должен сгенерировать правильный код, и неважно, какой алгоритм он для этого использует. То же самое относится и к вашему понятию аппаратного кеширования. Соответствующая реализация C ++ выдаст инструкции, чтобы оборудование не делало этого. Отсутствие необходимых инструкций делает реализацию несоответствующей; директивы cache не исключение. - person MSalters; 28.06.2010
comment
@MSalters: вы преувеличиваете ответственность компилятора. Программист, а не компилятор, должен выбрать наиболее подходящий способ решения проблем параллелизма на своем оборудовании. volatile - это способ сказать компилятору не изменять порядок записи, чтобы вы также не боролись с компилятором. Для такого разделения ответственности есть прекрасная причина: такие архитектуры, как Cell, содержат несколько инструкций синхронизации / ограничения / барьера с совершенно разными затратами и эффектами, и компилятор не имеет возможности узнать, какая из них наиболее подходит для данной ситуации. - person moonshadow; 28.06.2010
comment
@MSalters: прочтите книгу II по архитектуре PowerPC разделы 3.2–3.3, и подумайте, что должен выдавать компилятор, соответствующий вашей интерпретации спецификации C ++. У компилятора просто нет информации для разумного решения (была ли страница памяти настроена с комбинированием записи? Вы записываете в память, совместно используемую потоками одного и того же ядра PPU, или на какое-то устройство, на которое влияет eieio, но не lwsync ?), поэтому спецификация не требует этого, а реальные компиляторы этого не делают. - person moonshadow; 28.06.2010
comment
@moonshadow: Меня это волнует, и компилятор Cell должен предоставить для этого расширение. Но если я просто скажу volatile, я хочу, чтобы компилятор делал то, что ему говорит стандарт (генерировал наблюдаемые чтения и записи), не утруждая меня деталями. Следует ли помещать изменчивые переменные на страницу eieo, но не lwsync? Меня это не волнует. - person MSalters; 28.06.2010
comment
@MSalters Критичным является то, что их чтение и запись являются наблюдаемыми побочными эффектами. Это утверждение о способе определения языка, а не о фактическом коде, который должна выдать реализация. Каждая реализация определяет, что это означает. Большинство из них выполняет простую загрузку и сохранение для чтения и записи изменчивых переменных. - person curiousguy; 02.10.2011
comment
должны выполняться явные инструкции, чтобы гарантировать сброс состояния. Есть ли случай, когда ОС не будет выполнять эти инструкции? - person curiousguy; 02.10.2011
comment
@MSalters Критично то, что их чтение и запись являются наблюдаемыми побочными эффектами. Наблюдаемы откуда? Кто наблюдатель? - person curiousguy; 18.10.2011
comment
@curiousguy: Не указано стандартом. Стандарт говорит только о observable behavior. Это подмножество поведения, в котором стандарт не допускает отклонений в соответствии с правилом «как если бы». Следовательно, пропуск чтения переменной из памяти разрешен правилом «как если бы» , если это чтение не является наблюдаемым поведением = ›volatile. - person MSalters; 18.10.2011
comment
@MSalters Таким образом, это не говорит о том, что вы можете наблюдать изменчивое хранилище / загрузку из другого потока. Или из ОЗУ. Или с какого-то устройства. Вы можете наблюдать за загрузкой / хранением с помощью эмулятора процессора. С отладчиком. Вы можете увидеть текущее состояние изменчивых переменных с помощью обработчика сигнала, отправленного в правый поток. - person curiousguy; 19.10.2011
comment
@curiousguy: возможно, ты захочешь задать свой вопрос; комментарии - не то место, где можно получить ответ. - person MSalters; 20.10.2011

Если я правильно понял соответствующие разделы, C ++ 0X не будет гарантировать его для автономной переменной или даже для изменчивой (volatile не предназначен для этого использования), но представит атомарные типы, для которых у вас будет гарантия. (см. заголовок <atomic>).

person AProgrammer    schedule 25.06.2010

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

Поскольку вы явно говорите «без мьютексов», pthreads не применяется.

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

person R Samuel Klatchko    schedule 24.06.2010
comment
Не в этом примере, если yield () - это функция, тело которой компилятор не видит, а переменная не является локальной для модуля компиляции, поскольку компилятор должен предположить, что функция yield () может изменить значение v. @ Ответ moonshadow, конечно, по-прежнему применим. - person CesarB; 25.06.2010
comment
N.B. этот ответ больше не актуален. У C ++ есть модель памяти, которой сегодня придерживаются большинство компиляторов (и они используют ту же модель даже для кода C ++ 03, потому что это единственная переносимая и четко определенная модель, которая у нас есть). Даже до C ++ 11 использование volatile для этого было непереносимым, поскольку компиляторы не соглашались с его значением. Чтобы написать правильный многопоточный код C ++ до модели памяти C ++ 11, необходимо было использовать встроенные функции, специфичные для платформы, для атомарных операций, а не полагаться на volatile. - person Jonathan Wakely; 02.12.2015

Это потенциальная гонка за данными.

Что касается потока POSIX, это UB. Я считаю, что то же самое и с C ++.

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

person curiousguy    schedule 02.10.2011

Гарантированно ли завершение потока 2 в конечном итоге? Или он может вращаться вечно, потому что когерентность кеша никогда не срабатывает и делает назначение видимым в кэше потока 2?

Если переменная не является изменчивой, у вас нет никаких гарантий. До C ++ 0x в стандарте просто нечего сказать о потоках, и, поскольку переменная не является изменчивой, чтение / запись не считаются наблюдаемыми побочными эффектами, поэтому компилятору разрешено обмануть. После C ++ 0x это состояние гонки, которое явно указано как неопределенное поведение.

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

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

Так что нет вообще, корректная работа не гарантируется даже с volatile. Может и, вероятно, часто будет, но не всегда (и это зависит от того, что происходит после цикла). Это зависит от того, насколько далеко компилятор готов зайти в оптимизации. Но разрешено зайти достаточно далеко, чтобы взломать код. Так что не надейтесь на это. Если вы хотите синхронизировать что-то подобное, используйте барьеры памяти. Вот для чего они нужны. (И если вы сделаете это, вам больше не понадобится volatile)

person jalf    schedule 25.06.2010
comment
+1, у меня есть только одна придирка: volatile только гарантирует, что компилятор не изменит порядок доступа к w.r.t. другие изменчивые доступы. Если переменная, помеченная как энергозависимая, находится в основной памяти (а не в аппаратном обеспечении с отображением в память), тогда ЦП все еще может изменить порядок доступа, потому что ЦП не знает, сказал ли исходный код, что он был энергозависимым или нет. Что касается процессора, то это просто адрес в памяти. Это означает, что нельзя полагаться на изменчивые переменные для обеспечения последовательной согласованности. Я полностью согласен с тем, что как только вы добавляете барьеры для обеспечения правильного упорядочивания, volatile не требуется, поэтому в первую очередь это бесполезно. - person Jonathan Wakely; 02.12.2015

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

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

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

person Jens Gustedt    schedule 25.06.2010