C++ Thread, общие данные

У меня есть приложение, в котором запущено 2 потока ... Есть ли какая-то гарантия, что когда я изменяю глобальную переменную из одного потока, другой заметит это изменение? У меня нет какой-либо системы синхронизации или взаимного исключения ... но должен ли этот код работать все время (представьте глобальный bool с именем dataUpdated):

Поток 1:

while(1) {
    if (dataUpdated)
        updateScreen();
    doSomethingElse();
}

Поток 2:

while(1) {
    if (doSomething())
        dataUpdated = TRUE;
}

Оптимизирует ли компилятор, подобный gcc, этот код таким образом, что он не проверяет глобальное значение, считая его только значением во время компиляции (потому что он никогда не изменяется в том же направлении)?

PS: Поскольку это приложение, похожее на игру, на самом деле не имеет значения, будет ли чтение во время записи значения ... все, что имеет значение, - это то, что изменение будет замечено другим потоком.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
29
0
30 445
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

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

volatile int myInteger;

Это работает только в модели памяти Java 1.5+. Стандарт C++ не рассматривает многопоточность, а volatile не гарантирует когерентность памяти между процессорами. Для этого вам нужен барьер памяти.

Chris Jester-Young 23.09.2008 03:29

Сам стандарт C++ в настоящее время не определяет эту семантику. Но все «недавние» компиляторы считают volatile правильной подсказкой.

Christopher 23.09.2008 03:38

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

puetzk 23.09.2008 03:56

puetzk: конечно, но в данном случае это не нужно.

wnoise 23.09.2008 04:07

Зависит от того, записывает ли doSomething () данные, которые будет читать updateScreen (). Это кажется вероятным, хотя он не упомянул об этом конкретно.

puetzk 23.09.2008 04:16

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

MSalters 23.09.2008 11:26

Нет, не уверен. Если вы объявляете переменную volatile, компилятор должен генерировать код, который всегда загружает переменную из памяти при чтении.

Помимо прочего, ваше решение будет использовать 100% ЦП. Google по запросу "условная переменная".

это пока только для демонстрационных целей

fabiopedrosa 23.09.2008 03:27

Крис Джестер-Янг отметил, что:

This only work under Java 1.5+'s memory model. The C++ standard does not address threading, and volatile does not guarantee memory coherency between processors. You do need a memory barrier for this

в таком случае единственный верный ответ - это реализация системы синхронизации, не так ли?

Некоторые комментарии намекают на использование взаимосвязанных / атомарных переменных. Это нормально: вы используете функции сравнения и установки для работы с ними, и они могут быть сложными в использовании. В противном случае для максимального удобства использования используйте замок.

Chris Jester-Young 23.09.2008 03:37

почему-то никто не согласился, потому что они похоронили этот ответ и комментарии ... Все равно спасибо: D

fabiopedrosa 23.09.2008 03:38

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

1800 INFORMATION 23.09.2008 03:40

Я могу выбрать только один ответ. Поскольку теперь я точно знаю, что мой компилятор учитывает ключевое слово volatile, я выберу его. Но я помню, что внедрение блокировок было бы приемлемым решением.

fabiopedrosa 23.09.2008 03:43

Вот статья Microsoft о барьерах памяти и нестабильности: msdn.microsoft.com/en-us/library/f20w0x5e(VS.80).aspx Похоже, у вас все будет хорошо, если вы используете компилятор Microsoft.

Adam Pierce 23.09.2008 03:56

Неа. Это говорит о том, что глобальные переменные подчиняются барьерам памяти, а локальные - пока они изменчивы. Он не говорит, что volatile включает является барьером памяти, и, насколько мне известно, это не так.

puetzk 23.09.2008 04:04

Барьеры памяти вызывают беспокойство только (когда-либо), когда мы говорим о ПОРЯДКЕ операций с памятью, наблюдаемых ДРУГИМИ процессорами на реальной системной шине. Это происходит из-за того, что ЦП (а не компилятор в данном случае) может изменять порядок доступа к памяти на лету. Для одной флаговой переменной здесь это не проблема.

Tall Jeff 23.09.2008 04:18

Вот пример, в котором используются переменные условия повышения:

bool _updated=false;
boost::mutex _access;
boost::condition _condition;

bool updated()
{
  return _updated;
}

void thread1()
{
  boost::mutex::scoped_lock lock(_access);
  while (true)
  {
    boost::xtime xt;
    boost::xtime_get(&xt, boost::TIME_UTC);
    // note that the second parameter to timed_wait is a predicate function that is called - not the address of a variable to check
    if (_condition.timed_wait(lock, &updated, xt))
      updateScreen();
    doSomethingElse();
  }
}

void thread2()
{
  while(true)
  {
    if (doSomething())
      _updated=true;
  }
}

Если область видимости правильная («внешняя», глобальная и т. д.), То изменение будет замечено. Вопрос в том, когда? И в каком порядке?

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

На самом деле это не отображается в вашем конкретном примере, потому что нет никаких других инструкций вокруг вашего присваивания, но представьте, что функции, объявленные после вашего bool assign, выполняют перед присваивания.

Загляните в Опасность трубопроводов в Википедии или поищите в Google по запросу "изменение порядка инструкций компилятора"

Используйте замок. Всегда всегда используйте блокировку для доступа к общим данным. Пометка переменной как изменчивой не позволит компилятору оптимизировать чтение из памяти, но не предотвратит другие проблемы, такие как переупорядочение памяти. Без блокировки нет гарантии, что данные, записанные в doSomething (), будут видны в функции updateScreen ().

Единственный другой безопасный способ - использовать забор памяти, явно или неявно, например, с помощью функции Interlocked *.

Как уже говорили другие, ключевое слово volatile - ваш друг. :-)

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

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

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

При всем вышесказанном вы можете получить лучшие результаты (как указано в Джефф) за счет использования семафора или условной переменной.

Этот - разумное введение в предмет.

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

Да. Нет. Может быть.

Во-первых, как уже упоминалось другими, вам нужно сделать dataUpdated изменчивым; в противном случае компилятор может освободить чтение из цикла (в зависимости от того, видит ли он, что doSomethingElse его не касается).

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

Летучие вещества считаются вредными и Барьеры памяти ядра Linux - хороший фон по основным проблемам; Я не знаю ничего подобного, написанного специально для многопоточности. К счастью, потоки не вызывают этих проблем почти так часто, как аппаратные периферийные устройства, хотя описываемый вами тип случая (флаг, указывающий на завершение, с другими данными, которые считаются действительными, если этот флаг установлен), является именно тем, что требует упорядочивания. материя ...

Это дополнительная полезная информация, которую следует учитывать в дополнение к тем моментам, которые я сделал в своем ответе здесь.

Tall Jeff 23.09.2008 04:27

«Если вам нужны и используются барьеры памяти, вам больше не понадобится volatile» Эм ... разве вам все равно не понадобится volatile, чтобы компилятор не кэшировал переменную в регистре для цикла, который, например, считывает значение? Из того же ответа: «вам нужно сделать dataUpdated изменчивым; в противном случае компилятор может освободить чтение из цикла». Я не понимаю, как в этом помогает барьер памяти.

James Johnston 12.10.2011 22:27

В любом случае барьеры памяти должны быть реализованы с помощью компилятора. Если вы попросите компилятор убедиться, что ЦП не может выполнить загрузку / сохранение именно в этом порядке, то компилятор должен начать с выполнения самого порядка. Таким образом, барьер подразумевает все, что делает изменчивый, и многое другое.

puetzk 26.08.2013 17:22

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

volatile int myInteger;

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

Крис Джестер-Янг отметил, что проблемы согласованности при таком изменении значения переменной могут возникнуть в многопроцессорных системах. Это необходимо учитывать и зависит от платформы.

На самом деле, есть два момента, о которых следует подумать относительно платформы. Это согласованность и атомарность транзакций памяти.

Атомарность действительно важна как для однопроцессорных, так и для многопроцессорных платформ. Проблема возникает из-за того, что переменная, вероятно, имеет многобайтовый характер, и вопрос в том, может ли один поток видеть частичное обновление значения или нет. т.е.: изменены некоторые байты, переключение контекста, недопустимое значение, прочитанное прерывающим потоком. Для одной переменной, которая имеет размер машинного слова или меньше и естественно выровнена, не должно быть проблемой. В частности, тип int всегда должен быть в порядке в этом отношении, пока он выровнен - ​​что должно быть случаем по умолчанию для компилятора.

Что касается согласованности, это потенциальная проблема в многопроцессорной системе. Вопрос в том, реализует ли система полную когерентность кеша между процессорами или нет. Если реализовано, это обычно делается с помощью протокола MESI на оборудовании. В вопросе не говорится о платформах, но как платформы Intel x86, так и платформы PowerPC имеют согласованный кэш между процессорами для обычно отображаемых областей данных программы. Поэтому проблемы этого типа не должны беспокоить при обычном доступе к памяти данных между потоками, даже если имеется несколько процессоров.

Последняя проблема, связанная с атомарностью, связана с атомарностью чтения-изменения-записи. То есть, как вы гарантируете, что если значение считывается, обновляется по значению и записывается, что это происходит атомарно, даже между процессорами, если их больше одного. Итак, для того, чтобы это работало без определенных объектов синхронизации, потребовалось бы, чтобы все потенциальные потоки, обращающиеся к переменной, были ТОЛЬКО читателями, но ожидайте, что только один поток может когда-либо быть писателем одновременно. Если это не так, то вам действительно нужен доступный объект синхронизации, чтобы иметь возможность гарантировать атомарные действия при действиях чтения-изменения-записи для переменной.

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