Я определяю переменные как
volatile sig_atomic_t v1;
volatile int v2;
Затем в одном процессе (потоке) выполните
v1 = false;
v2 = 0;
...
v2 = (some_var);
v1 = true;
и в другом процессе (потоке)
while(!v1) { (doing some tasks not related to v1 and v2); }
if (v2 == 0) { (do something); }
Проблема, с которой я сталкиваюсь, заключается в том, что v2 иногда устанавливается на 0, когда while заканчивается. Доказано, что while
заканчивается, когда v1
становится правдой. Доказано, что (some_var)
не равно нулю. Использование gcc.
Что мне здесь не хватает?
Это происходит, даже если отключить оптимизацию?
@Barmar Не знаю, где посмотреть (где сгенерированный код - сборка?) Как отключить оптимизации? Еще я внес поправку, что хоть и не пусто, но внутри что-то есть. Я подозреваю, что что-то делаю неправильно, вызывая такие эффекты, но не уверен, что это проблема компилятора.
См. stackoverflow.com/questions/137038/…, чтобы узнать, как сохранить выходные данные сборки.
возможно, что v1 = 0;
происходит в другом потоке между концом цикла while
и оператором if
.
volatile
синхронизация потоков неверна. Вместо этого вам понадобится _Atomic
(для C) или std::atomic
(для C++). Пожалуйста, отметьте язык, о котором вы спрашиваете. volatile
не гарантирует ни атомарность, ни какое-либо упорядочение памяти, кроме как в отношении другого volatile
доступа в том же потоке. Это также верно для sig_atomic_t
, который имеет специальные свойства только в отношении обработчиков сигналов в одном потоке, а не между несколькими потоками.
@ user17732522 Я думал, что volatile
для всего приложения означает область памяти для чтения/записи каждый раз, когда это необходимо для тех, кто знает об этом модификаторе, а sig_atomic_t
означает выполнение операций за один цикл (или любым другим атомарным способом) для устранения условий гонки. Поэтому, если я обновлю какой-то флаг после обновления других переменных, другой поток догонит эти переменные после того, как увидит обновленный флаг.
@Anonymous «чтение/запись ячейки памяти каждый раз, когда это необходимо для тех, кто знает об этом модификаторе»: я не совсем понимаю, что вы имеете в виду. volatile
означает только то, что компилятор должен строго выполнять загрузку или сохранение в память, как это происходит на абстрактной машине, и что все эти загрузки и сохранения должны происходить в том же порядке, что и на абстрактной машине. Это не подразумевает ничего об аотности или порядке памяти по отношению к другим загрузкам/сохранениям. Компилятор и процессор могут изменять их порядок по своему усмотрению.
@Anonymous volatile sig_atomic_t
не означает, что операцию необходимо выполнять за один цикл. Он гарантирует только атомарность по отношению к обработчикам сигналов в том же потоке и то, что его значение, видимое обработчиком сигнала, будет задано и согласовано с состоянием вызывающего потока. Это ничего не гарантирует в отношении других потоков.
То, что для достижения этих гарантий загрузка и сохранение volatile sig_atomic_t
должны происходить как отдельные инструкции (что также не то же самое, что циклы), является деталью реализации компилятора.
@Anonymous Прочтите это по теме: volatile
: Изменчивость: почти бесполезна для многопоточного программирования и это: Должна ли волатильность приобретать атомарность и семантику видимости потоков?Н.Б. подразумевается, что volatile
поэтому не хватает какой-либо атомарности и семантики видимости потока...
Какая архитектура процессора? В слабоупорядоченных архитектурах (например, ARM), даже если инструкции загрузки и сохранения выполняются в правильном порядке, ЦП все равно может изменить их порядок, так что другой поток может видеть их в другом порядке. Чтобы избежать этого, требуются инструкции по барьеру памяти, которые volatile
не вставляются. Как говорит Эндрю, предполагаемое решение — _Atomic
.
К сожалению, ваш код и комментарии к volatile
предполагают, что вы придерживаетесь множества заблуждений относительно модели памяти C для многопоточности (после C11). Возможно, вам придется много раз учиться и переучиваться, прежде чем вернуться к написанию кода.
@NateEldredge Это не я придумал, см. en.wikipedia.org/wiki/Volatile_(computer_programming) но дальше по тексту написано C voltaile
не тот, что описан в шапке. Это ИМХО просто сломано.
Вы видели абзац, который начинается со слов «Операции с изменчивыми переменными не являются атомарными и не устанавливают правильную связь «происходит до» для потоковой передачи»? Фактически, в статье снова и снова говорится, что volatile
не подходит для переменных, общих для нескольких потоков.
volatile
не имеет ничего общего с атомным доступом.
Предотвращение выполнения компилятором странных оптимизаций, когда он не понимает, что обратный вызов вызывается из ОС, а не из программы, поможет вам только до определенного момента - на самом деле компиляторы, подобные ПК, такие как gcc, достаточно умны, чтобы знать, что обратные вызовы могут быть вызваны, поэтому volatile
по переменным, совместно используемым между потоками, вероятно, не добавляет ничего значимого к обычному приложению GCC Linux. (Однако так будет в том случае, если вашей целью является какой-нибудь более или менее неблагополучный компилятор встраиваемых систем.)
Однако по-прежнему остается основная проблема повторного входа и защиты от ошибок состояния гонки. Посмотрите, например Использование voluty при разработке встроенных программ на C - речь идет о процедурах обслуживания прерываний, но они ведут себя так же, как потоки, когда дело доходит до повторного входа. Замените «ISR» на ветку в моем ответе и сделайте акцент на последнем предложении:
При написании C весь обмен данными между ISR и фоновой программой должен быть защищен от условий гонки. Всегда, всегда, без исключений. Размер шины данных MCU не имеет значения, потому что даже если вы сделаете одну 8-битную копию на C, язык не может гарантировать атомарность операций. Нет, если вы не используете функцию C11
_Atomic
. Если эта функция недоступна, вы должны использовать какой-либо семафор или отключить прерывание во время чтения и т. д. Другой вариант — встроенный ассемблер.volatile
не гарантирует атомарность.Что может произойти, так это:
- Загрузить значение из стека в регистр
- Происходит прерывание
- Использовать значение из регистра
И тогда не имеет значения, является ли часть «использованное значение» сама по себе одной инструкцией. К сожалению, значительная часть всех программистов встраиваемых систем не обращает на это внимания, что, вероятно, делает это самой распространенной ошибкой встроенных систем за всю историю. Всегда с перерывами, его трудно спровоцировать, его трудно найти.
Такое ощущение, что я написал сотни ответов по этому поводу. Вот ещё:
Что
volatile
делает:
- Гарантирует актуальное значение переменной, если переменная изменяется из внешнего источника (аппаратный регистр, прерывание, другой поток, функция обратного вызова и т. д.).
- Блокирует все оптимизации доступа для чтения/записи к переменной.
- Предотвратите опасные ошибки оптимизации, которые могут произойти с переменными, совместно используемыми несколькими потоками/прерываниями/функциями обратного вызова, когда компилятор не осознает, что вызывается поток/прерывание/обратный вызов по программе. (Это особенно распространено среди различных сомнительные компиляторы встраиваемых систем, и когда вы получаете эту ошибку, очень сложно отследить)
Чего
volatile
нет:
- Он не гарантирует атомарный доступ или какую-либо форму потокобезопасности.
- Его нельзя использовать вместо мьютекса/семафора/защитного/критического раздела. Его нельзя использовать для синхронизации потоков.
Что
volatile
можно или нельзя делать:
- Это может быть реализовано или не реализовано компилятором для обеспечения барьера памяти для защиты от кэша/инструкций команд. проблемы с переупорядочением каналов/инструкций в многоядерной среде. Ты никогда не следует предполагать, что Летучий сделает это за вас, если только в документации компилятора прямо указано, что это так.
Что касается части барьера памяти и различных возможных способов чтения/неправильной интерпретации стандарта C:
Почему барьер памяти запрещает оптимизацию статической глобальной переменной?
Большое спасибо! Что насчет volatile atomic_int
? Кажется, не вызывает предупреждений и работает (пока) без симптомов, которые я описал в вопросе.
@Anonymous atomic_int
и _Atomic int
— это одно и то же, если вы об этом. Это действительный стандарт C, соответствующий C11 или более поздней версии.
@Anonymous Вам не нужен volatile
, если вас интересует только синхронизация потоков (не считая ошибок компилятора, упомянутых в этом ответе). Ничего полезного для этого не будет. Как говорится в этом ответе, вам нужен volatile
, если местоположение памяти может быть изменено извне и/или точная последовательность загрузки/сохранения в память имеет отношение к внешней программе. Однако вам нужно позаботиться о том, чтобы использовать достаточно строгий порядок памяти при доступе к atomic_int
, то есть в вашем случае использования, по крайней мере, получить при загрузке выпуск в магазине.
@Anonymous И только v1
, кстати. v2
не обязательно должен быть volatile
или атомарным (по крайней мере, исходя только из того использования, которое вы представили).
@user17732522 user17732522, предположительно, проблема заключалась в том, что версия 2 была написана позже, чем версия 1, поэтому второй код мог читать 0 из версии 2 в тот момент, когда ему нужна эта версия 2. Поэтому оба должны быть максимально надежными, чтобы предотвратить изменение порядка выполнения и позднюю запись/обновление.
@Anonymous Нет, это неправда. Упорядочение памяти при загрузке/сохранении v1
— это все, что вам нужно. Это означает, что сохранение v2
, которое упорядочено перед сохранением v1
в первом потоке, произойдет до загрузки v2
во втором потоке, если загрузка v1
получает свое значение из ранее упомянутого хранилища в первом потоке, потому что v2
секвенируется — после загрузки v1
во втором потоке. То есть при условии, что загрузка и сохранение v1
имеют порядок приобретения/освобождения памяти. См., например. en.cppreference.com/w/c/atomic/memory_order.
@Anonymous И если вместо этого вы используете только расслабленный порядок, то атомарность обоих также не спасет вас от изменения порядка. Выбор порядка памяти — это основная проблема, которую вам необходимо решить, чтобы избежать ошибки в вашей программе. И, как уже упоминалось в комментарии к вопросу, похоже, что вы неправильно думаете с точки зрения модели памяти C, что означает порядок памяти. Речь не идет (по сути) об оптимизации компилятора. Компиляторы, способные оптимизировать работу с учетом модели памяти, — это всего лишь побочный эффект от абстрагирования реальных моделей аппаратной памяти.
@Anonymous Конечно, если первый поток продолжает писать в v2
даже после того, как его хранилище true
в v1
, то вам также нужно подумать о синхронизации этого хранилища с вашей загрузкой. И тогда у вас снова возникла бы гонка данных, которая _Atomic
на v2
помешала бы.
@user17732522 user17732522 Я думаю, что понял, но единственное жизнеспособное решение (а не обходной путь), которое я вижу, - это сменить платформу на разработанный мною процессор на базе FPGA, который намеренно будет лишен таких проблем. С ARM нет контроля над тем, что процессор хочет делать. Таким образом, я все равно должен принять этот риск.
@Anonymous Жизнеспособное решение — просто использовать _Atomic
правильно в соответствии со спецификацией. Это будет прекрасно работать и на ARM. Это становится сложнее, только если у вас нет компилятора или платформы, поддерживающей _Atomic
.
Нет, смысл был в том, чтобы загрузить данные в структуру и затем установить флаг. Периодически это не получалось. На более медленных системах частота отказов была ниже, на более быстрых — почти мгновенная. Мне нужен способ остановить этот кошмар и гарантировать, что первый поток выполняет то, что я прошу, и второй, чтобы получить результаты, когда он заметит установленный флаг (v1 в моем примере). В этой модели нет проблем, если второй поток однажды прочитает флаг как ложный (из-за метастабильности), в следующий раз он прочтет его правильно.
@Anonymous Ну да, потому что ты никогда не говорил, в каком порядке это делать. Нет очевидного сопоставления порядка операторов/выражений исходного кода с представлением памяти оборудования. Вы сообщаете компилятору и процессору необходимый вам порядок, делая v1
например. atomic_bool
, заменив v1 = true;
на atomic_store_explicit(&v1, true, memory_order_release);
и while(!v1)
на while(!atomic_load_explicit(&v1, memory_order_acquire))
, и тогда ваша программа будет иметь нужную вам семантику и будет работать так, как вы этого хотите. (Несмотря на то, что другие расы в коде вы не показали.)
Если вы посмотрите на сгенерированный код, оптимизирован ли он для цикла
while
?