Проблема оптимизации компилятора / переменная имеет неправильное значение

Я определяю переменные как

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.

Что мне здесь не хватает?

Если вы посмотрите на сгенерированный код, оптимизирован ли он для цикла while?

Barmar 08.07.2024 21:15

Это происходит, даже если отключить оптимизацию?

Barmar 08.07.2024 21:15

@Barmar Не знаю, где посмотреть (где сгенерированный код - сборка?) Как отключить оптимизации? Еще я внес поправку, что хоть и не пусто, но внутри что-то есть. Я подозреваю, что что-то делаю неправильно, вызывая такие эффекты, но не уверен, что это проблема компилятора.

Anonymous 08.07.2024 21:29

См. stackoverflow.com/questions/137038/…, чтобы узнать, как сохранить выходные данные сборки.

Barmar 08.07.2024 21:30

возможно, что v1 = 0; происходит в другом потоке между концом цикла while и оператором if.

Barmar 08.07.2024 21:31
volatile синхронизация потоков неверна. Вместо этого вам понадобится _Atomic (для C) или std::atomic (для C++). Пожалуйста, отметьте язык, о котором вы спрашиваете. volatile не гарантирует ни атомарность, ни какое-либо упорядочение памяти, кроме как в отношении другого volatile доступа в том же потоке. Это также верно для sig_atomic_t, который имеет специальные свойства только в отношении обработчиков сигналов в одном потоке, а не между несколькими потоками.
user17732522 08.07.2024 21:37

@ user17732522 Я думал, что volatile для всего приложения означает область памяти для чтения/записи каждый раз, когда это необходимо для тех, кто знает об этом модификаторе, а sig_atomic_t означает выполнение операций за один цикл (или любым другим атомарным способом) для устранения условий гонки. Поэтому, если я обновлю какой-то флаг после обновления других переменных, другой поток догонит эти переменные после того, как увидит обновленный флаг.

Anonymous 08.07.2024 23:02

@Anonymous «чтение/запись ячейки памяти каждый раз, когда это необходимо для тех, кто знает об этом модификаторе»: я не совсем понимаю, что вы имеете в виду. volatile означает только то, что компилятор должен строго выполнять загрузку или сохранение в память, как это происходит на абстрактной машине, и что все эти загрузки и сохранения должны происходить в том же порядке, что и на абстрактной машине. Это не подразумевает ничего об аотности или порядке памяти по отношению к другим загрузкам/сохранениям. Компилятор и процессор могут изменять их порядок по своему усмотрению.

user17732522 09.07.2024 00:05

@Anonymous volatile sig_atomic_t не означает, что операцию необходимо выполнять за один цикл. Он гарантирует только атомарность по отношению к обработчикам сигналов в том же потоке и то, что его значение, видимое обработчиком сигнала, будет задано и согласовано с состоянием вызывающего потока. Это ничего не гарантирует в отношении других потоков.

user17732522 09.07.2024 00:10

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

user17732522 09.07.2024 00:10

@Anonymous Прочтите это по теме: volatile: Изменчивость: почти бесполезна для многопоточного программирования и это: Должна ли волатильность приобретать атомарность и семантику видимости потоков?Н.Б. подразумевается, что volatile поэтому не хватает какой-либо атомарности и семантики видимости потока...

Andrew Henle 09.07.2024 01:38

Какая архитектура процессора? В слабоупорядоченных архитектурах (например, ARM), даже если инструкции загрузки и сохранения выполняются в правильном порядке, ЦП все равно может изменить их порядок, так что другой поток может видеть их в другом порядке. Чтобы избежать этого, требуются инструкции по барьеру памяти, которые volatile не вставляются. Как говорит Эндрю, предполагаемое решение — _Atomic.

Nate Eldredge 09.07.2024 04:29

К сожалению, ваш код и комментарии к volatile предполагают, что вы придерживаетесь множества заблуждений относительно модели памяти C для многопоточности (после C11). Возможно, вам придется много раз учиться и переучиваться, прежде чем вернуться к написанию кода.

Nate Eldredge 09.07.2024 04:30

@NateEldredge Это не я придумал, см. en.wikipedia.org/wiki/Volatile_(computer_programming) но дальше по тексту написано C voltaile не тот, что описан в шапке. Это ИМХО просто сломано.

Anonymous 09.07.2024 07:30

Вы видели абзац, который начинается со слов «Операции с изменчивыми переменными не являются атомарными и не устанавливают правильную связь «происходит до» для потоковой передачи»? Фактически, в статье снова и снова говорится, что volatile не подходит для переменных, общих для нескольких потоков.

Nate Eldredge 09.07.2024 07:32
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
15
112
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

volatile не имеет ничего общего с атомным доступом.

Предотвращение выполнения компилятором странных оптимизаций, когда он не понимает, что обратный вызов вызывается из ОС, а не из программы, поможет вам только до определенного момента - на самом деле компиляторы, подобные ПК, такие как gcc, достаточно умны, чтобы знать, что обратные вызовы могут быть вызваны, поэтому volatile по переменным, совместно используемым между потоками, вероятно, не добавляет ничего значимого к обычному приложению GCC Linux. (Однако так будет в том случае, если вашей целью является какой-нибудь более или менее неблагополучный компилятор встраиваемых систем.)

Однако по-прежнему остается основная проблема повторного входа и защиты от ошибок состояния гонки. Посмотрите, например Использование voluty при разработке встроенных программ на C - речь идет о процедурах обслуживания прерываний, но они ведут себя так же, как потоки, когда дело доходит до повторного входа. Замените «ISR» на ветку в моем ответе и сделайте акцент на последнем предложении:

При написании C весь обмен данными между ISR и фоновой программой должен быть защищен от условий гонки. Всегда, всегда, без исключений. Размер шины данных MCU не имеет значения, потому что даже если вы сделаете одну 8-битную копию на C, язык не может гарантировать атомарность операций. Нет, если вы не используете функцию C11 _Atomic. Если эта функция недоступна, вы должны использовать какой-либо семафор или отключить прерывание во время чтения и т. д. Другой вариант — встроенный ассемблер. volatile не гарантирует атомарность.

Что может произойти, так это:

  • Загрузить значение из стека в регистр
  • Происходит прерывание
  • Использовать значение из регистра

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

Такое ощущение, что я написал сотни ответов по этому поводу. Вот ещё:

Что volatile делает:

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

Чего volatile нет:

  • Он не гарантирует атомарный доступ или какую-либо форму потокобезопасности.
  • Его нельзя использовать вместо мьютекса/семафора/защитного/критического раздела. Его нельзя использовать для синхронизации потоков.

Что volatile можно или нельзя делать:

  • Это может быть реализовано или не реализовано компилятором для обеспечения барьера памяти для защиты от кэша/инструкций команд. проблемы с переупорядочением каналов/инструкций в многоядерной среде. Ты никогда не следует предполагать, что Летучий сделает это за вас, если только в документации компилятора прямо указано, что это так.

Что касается части барьера памяти и различных возможных способов чтения/неправильной интерпретации стандарта C:
Почему барьер памяти запрещает оптимизацию статической глобальной переменной?

Большое спасибо! Что насчет volatile atomic_int? Кажется, не вызывает предупреждений и работает (пока) без симптомов, которые я описал в вопросе.

Anonymous 09.07.2024 11:09

@Anonymous atomic_int и _Atomic int — это одно и то же, если вы об этом. Это действительный стандарт C, соответствующий C11 или более поздней версии.

Lundin 09.07.2024 11:17

@Anonymous Вам не нужен volatile, если вас интересует только синхронизация потоков (не считая ошибок компилятора, упомянутых в этом ответе). Ничего полезного для этого не будет. Как говорится в этом ответе, вам нужен volatile, если местоположение памяти может быть изменено извне и/или точная последовательность загрузки/сохранения в память имеет отношение к внешней программе. Однако вам нужно позаботиться о том, чтобы использовать достаточно строгий порядок памяти при доступе к atomic_int, то есть в вашем случае использования, по крайней мере, получить при загрузке выпуск в магазине.

user17732522 09.07.2024 12:28

@Anonymous И только v1, кстати. v2 не обязательно должен быть volatile или атомарным (по крайней мере, исходя только из того использования, которое вы представили).

user17732522 09.07.2024 12:32

@user17732522 user17732522, предположительно, проблема заключалась в том, что версия 2 была написана позже, чем версия 1, поэтому второй код мог читать 0 из версии 2 в тот момент, когда ему нужна эта версия 2. Поэтому оба должны быть максимально надежными, чтобы предотвратить изменение порядка выполнения и позднюю запись/обновление.

Anonymous 09.07.2024 12:51

@Anonymous Нет, это неправда. Упорядочение памяти при загрузке/сохранении v1 — это все, что вам нужно. Это означает, что сохранение v2, которое упорядочено перед сохранением v1 в первом потоке, произойдет до загрузки v2 во втором потоке, если загрузка v1 получает свое значение из ранее упомянутого хранилища в первом потоке, потому что v2 секвенируется — после загрузки v1 во втором потоке. То есть при условии, что загрузка и сохранение v1 имеют порядок приобретения/освобождения памяти. См., например. en.cppreference.com/w/c/atomic/memory_order.

user17732522 09.07.2024 12:57

@Anonymous И если вместо этого вы используете только расслабленный порядок, то атомарность обоих также не спасет вас от изменения порядка. Выбор порядка памяти — это основная проблема, которую вам необходимо решить, чтобы избежать ошибки в вашей программе. И, как уже упоминалось в комментарии к вопросу, похоже, что вы неправильно думаете с точки зрения модели памяти C, что означает порядок памяти. Речь не идет (по сути) об оптимизации компилятора. Компиляторы, способные оптимизировать работу с учетом модели памяти, — это всего лишь побочный эффект от абстрагирования реальных моделей аппаратной памяти.

user17732522 09.07.2024 13:00

@Anonymous Конечно, если первый поток продолжает писать в v2 даже после того, как его хранилище true в v1, то вам также нужно подумать о синхронизации этого хранилища с вашей загрузкой. И тогда у вас снова возникла бы гонка данных, которая _Atomic на v2 помешала бы.

user17732522 09.07.2024 13:03

@user17732522 user17732522 Я думаю, что понял, но единственное жизнеспособное решение (а не обходной путь), которое я вижу, - это сменить платформу на разработанный мною процессор на базе FPGA, который намеренно будет лишен таких проблем. С ARM нет контроля над тем, что процессор хочет делать. Таким образом, я все равно должен принять этот риск.

Anonymous 09.07.2024 13:03

@Anonymous Жизнеспособное решение — просто использовать _Atomic правильно в соответствии со спецификацией. Это будет прекрасно работать и на ARM. Это становится сложнее, только если у вас нет компилятора или платформы, поддерживающей _Atomic.

user17732522 09.07.2024 13:04

Нет, смысл был в том, чтобы загрузить данные в структуру и затем установить флаг. Периодически это не получалось. На более медленных системах частота отказов была ниже, на более быстрых — почти мгновенная. Мне нужен способ остановить этот кошмар и гарантировать, что первый поток выполняет то, что я прошу, и второй, чтобы получить результаты, когда он заметит установленный флаг (v1 в моем примере). В этой модели нет проблем, если второй поток однажды прочитает флаг как ложный (из-за метастабильности), в следующий раз он прочтет его правильно.

Anonymous 09.07.2024 13:06

@Anonymous Ну да, потому что ты никогда не говорил, в каком порядке это делать. Нет очевидного сопоставления порядка операторов/выражений исходного кода с представлением памяти оборудования. Вы сообщаете компилятору и процессору необходимый вам порядок, делая v1 например. atomic_bool, заменив v1 = true; на atomic_store_explicit(&v1, true, memory_order_release); и while(!v1) на while(!atomic_load_explicit(&v1, memory_order_acquire)), и тогда ваша программа будет иметь нужную вам семантику и будет работать так, как вы этого хотите. (Несмотря на то, что другие расы в коде вы не показали.)

user17732522 09.07.2024 13:12

Другие вопросы по теме