У меня есть приложение, в котором запущено 2 потока ... Есть ли какая-то гарантия, что когда я изменяю глобальную переменную из одного потока, другой заметит это изменение? У меня нет какой-либо системы синхронизации или взаимного исключения ... но должен ли этот код работать все время (представьте глобальный bool с именем dataUpdated):
Поток 1:
while(1) {
if (dataUpdated)
updateScreen();
doSomethingElse();
}
Поток 2:
while(1) {
if (doSomething())
dataUpdated = TRUE;
}
Оптимизирует ли компилятор, подобный gcc, этот код таким образом, что он не проверяет глобальное значение, считая его только значением во время компиляции (потому что он никогда не изменяется в том же направлении)?
PS: Поскольку это приложение, похожее на игру, на самом деле не имеет значения, будет ли чтение во время записи значения ... все, что имеет значение, - это то, что изменение будет замечено другим потоком.





Используйте ключевое слово летучий, чтобы указать компилятору, что значение может измениться в любое время.
volatile int myInteger;
Сам стандарт C++ в настоящее время не определяет эту семантику. Но все «недавние» компиляторы считают volatile правильной подсказкой.
Нет, это не так. volatile гарантирует, что компилятор выполнит загрузку, как указано, и, таким образом, учитывает многопоточность, но не гарантирует, что базовая подсистема памяти сохранит причинно-следственную связь в настоящей многопроцессорной системе.
puetzk: конечно, но в данном случае это не нужно.
Зависит от того, записывает ли doSomething () данные, которые будет читать updateScreen (). Это кажется вероятным, хотя он не упомянул об этом конкретно.
Эти другие данные, конечно, должны быть непостоянными, чтобы в этот момент были две точки последовательности для двух записей. Это обеспечивает надлежащий порядок.
Нет, не уверен. Если вы объявляете переменную volatile, компилятор должен генерировать код, который всегда загружает переменную из памяти при чтении.
Помимо прочего, ваше решение будет использовать 100% ЦП. Google по запросу "условная переменная".
это пока только для демонстрационных целей
Крис Джестер-Янг отметил, что:
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
в таком случае единственный верный ответ - это реализация системы синхронизации, не так ли?
Некоторые комментарии намекают на использование взаимосвязанных / атомарных переменных. Это нормально: вы используете функции сравнения и установки для работы с ними, и они могут быть сложными в использовании. В противном случае для максимального удобства использования используйте замок.
почему-то никто не согласился, потому что они похоронили этот ответ и комментарии ... Все равно спасибо: D
Я отклонил этот ответ, потому что он был плохо сформулирован и сбивал с толку. Я подозреваю, что другие поступили так же.
Я могу выбрать только один ответ. Поскольку теперь я точно знаю, что мой компилятор учитывает ключевое слово volatile, я выберу его. Но я помню, что внедрение блокировок было бы приемлемым решением.
Вот статья Microsoft о барьерах памяти и нестабильности: msdn.microsoft.com/en-us/library/f20w0x5e(VS.80).aspx Похоже, у вас все будет хорошо, если вы используете компилятор Microsoft.
Неа. Это говорит о том, что глобальные переменные подчиняются барьерам памяти, а локальные - пока они изменчивы. Он не говорит, что volatile включает является барьером памяти, и, насколько мне известно, это не так.
Барьеры памяти вызывают беспокойство только (когда-либо), когда мы говорим о ПОРЯДКЕ операций с памятью, наблюдаемых ДРУГИМИ процессорами на реальной системной шине. Это происходит из-за того, что ЦП (а не компилятор в данном случае) может изменять порядок доступа к памяти на лету. Для одной флаговой переменной здесь это не проблема.
Вот пример, в котором используются переменные условия повышения:
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 - хороший фон по основным проблемам; Я не знаю ничего подобного, написанного специально для многопоточности. К счастью, потоки не вызывают этих проблем почти так часто, как аппаратные периферийные устройства, хотя описываемый вами тип случая (флаг, указывающий на завершение, с другими данными, которые считаются действительными, если этот флаг установлен), является именно тем, что требует упорядочивания. материя ...
Это дополнительная полезная информация, которую следует учитывать в дополнение к тем моментам, которые я сделал в своем ответе здесь.
«Если вам нужны и используются барьеры памяти, вам больше не понадобится volatile» Эм ... разве вам все равно не понадобится volatile, чтобы компилятор не кэшировал переменную в регистре для цикла, который, например, считывает значение? Из того же ответа: «вам нужно сделать dataUpdated изменчивым; в противном случае компилятор может освободить чтение из цикла». Я не понимаю, как в этом помогает барьер памяти.
В любом случае барьеры памяти должны быть реализованы с помощью компилятора. Если вы попросите компилятор убедиться, что ЦП не может выполнить загрузку / сохранение именно в этом порядке, то компилятор должен начать с выполнения самого порядка. Таким образом, барьер подразумевает все, что делает изменчивый, и многое другое.
Используйте ключевое слово летучий, чтобы указать компилятору, что значение может измениться в любое время.
volatile int myInteger;
Вышеупомянутое гарантирует, что любой доступ к переменной будет осуществляться в память и из памяти без какой-либо конкретной оптимизации, и в результате все потоки, работающие на одном процессоре, будут «видеть» изменения в переменной с той же семантикой, что и читаемый код.
Крис Джестер-Янг отметил, что проблемы согласованности при таком изменении значения переменной могут возникнуть в многопроцессорных системах. Это необходимо учитывать и зависит от платформы.
На самом деле, есть два момента, о которых следует подумать относительно платформы. Это согласованность и атомарность транзакций памяти.
Атомарность действительно важна как для однопроцессорных, так и для многопроцессорных платформ. Проблема возникает из-за того, что переменная, вероятно, имеет многобайтовый характер, и вопрос в том, может ли один поток видеть частичное обновление значения или нет. т.е.: изменены некоторые байты, переключение контекста, недопустимое значение, прочитанное прерывающим потоком. Для одной переменной, которая имеет размер машинного слова или меньше и естественно выровнена, не должно быть проблемой. В частности, тип int всегда должен быть в порядке в этом отношении, пока он выровнен - что должно быть случаем по умолчанию для компилятора.
Что касается согласованности, это потенциальная проблема в многопроцессорной системе. Вопрос в том, реализует ли система полную когерентность кеша между процессорами или нет. Если реализовано, это обычно делается с помощью протокола MESI на оборудовании. В вопросе не говорится о платформах, но как платформы Intel x86, так и платформы PowerPC имеют согласованный кэш между процессорами для обычно отображаемых областей данных программы. Поэтому проблемы этого типа не должны беспокоить при обычном доступе к памяти данных между потоками, даже если имеется несколько процессоров.
Последняя проблема, связанная с атомарностью, связана с атомарностью чтения-изменения-записи. То есть, как вы гарантируете, что если значение считывается, обновляется по значению и записывается, что это происходит атомарно, даже между процессорами, если их больше одного. Итак, для того, чтобы это работало без определенных объектов синхронизации, потребовалось бы, чтобы все потенциальные потоки, обращающиеся к переменной, были ТОЛЬКО читателями, но ожидайте, что только один поток может когда-либо быть писателем одновременно. Если это не так, то вам действительно нужен доступный объект синхронизации, чтобы иметь возможность гарантировать атомарные действия при действиях чтения-изменения-записи для переменной.
Это работает только в модели памяти Java 1.5+. Стандарт C++ не рассматривает многопоточность, а volatile не гарантирует когерентность памяти между процессорами. Для этого вам нужен барьер памяти.