С++ Thread, общие данные
У меня есть приложение, в котором работает 2 потока... Есть ли какой-либо certanty, что, когда я изменяю глобальную переменную из одного потока, другой заметил это изменение?
У меня нет никакой синхронизации или системы взаимного исключения на месте... но должен ли этот код работать все время (представьте себе глобальный bool с именем dataUpdated):
Тема 1:
while(1) {
if (dataUpdated)
updateScreen();
doSomethingElse();
}
Тема 2:
while(1) {
if (doSomething())
dataUpdated = TRUE;
}
Компилятор вроде gcc оптимизирует этот код таким образом, чтобы он не проверял глобальное значение, учитывая его значение во время компиляции (потому что он не изменился при одном и том же thred)?
PS: Как это для игрового приложения, действительно неважно, будет ли прочитано, пока значение будет написано... все, что имеет значение, - это то, что изменение замечено другим потоком.
Ответы
Ответ 1
Да. Нет. Может быть.
Во-первых, как говорили другие, вам нужно сделать dataUpdated volatile; в противном случае компилятор может свободно снимать его из цикла (в зависимости от того, может ли он видеть, что doSomethingElse не касается его).
Во-вторых, в зависимости от вашего процессора и потребностей в заказе вам могут потребоваться блокировки памяти. volatile достаточно, чтобы гарантировать, что другой процессор увидит изменение в конце концов, но недостаточно, чтобы гарантировать, что изменения будут видны в том порядке, в котором они были выполнены. В вашем примере есть только один флаг, поэтому он не показывает это явление. Если вам нужны и использовать барьеры памяти, вам больше не понадобится летучесть
Неустойчивый считается вредным и Ядра памяти ядра Linux хороший фон по основным вопросам; Я не знаю ничего подобного, написанного специально для потокования. К счастью, нити не поднимают эти проблемы почти так же часто, как и аппаратные периферийные устройства, хотя описанный вами случай (флаг, указывающий завершение, с другими данными, считающимися действительными, если установлен флаг) - это именно то, matterns...
Ответ 2
Вот пример, который использует переменные состояния форматирования:
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;
}
}
Ответ 3
Используйте блокировку. Всегда всегда используйте блокировку для доступа к общим данным. Маркировка переменной как volatile не позволит компилятору оптимизировать чтение памяти, но не предотвратит другие проблемы, такие как переупорядочение памяти. Без блокировки нет гарантии, что запись в памяти doSomething() будет видна в функции updateScreen().
Единственный безопасный способ - использовать забор памяти, явно или неявно, используя функцию Interlocked *, например.
Ответ 4
Используйте ключевое слово volatile, чтобы указать компилятору, что значение может измениться в любое время.
volatile int myInteger;
Вышеупомянутое гарантирует, что любой доступ к переменной будет в памяти и из памяти без каких-либо конкретных оптимизаций, и в результате все потоки, запущенные на одном процессоре, будут "видеть" изменения в переменной с той же семантикой, что и код, читаемый.
Chris Jester-Young отметил, что в многопроцессорных системах могут возникать проблемы согласованности с таким изменением переменных значений. Это соображение, и это зависит от платформы.
На самом деле, есть действительно два соображения, чтобы думать об отношении к платформе. Это когерентность и атомарность транзакций памяти.
Атоматичность на самом деле является соображением как для одноплатных, так и для многопроцессорных платформ. Проблема возникает из-за того, что переменная, вероятно, является многобайтовой по своей природе, и возникает вопрос, может ли один поток увидеть частичное обновление значения или нет. т.е.: Некоторые байты изменены, контекстный переключатель, недопустимое значение, считанное прерыванием потока. Для одной переменной, которая находится на натуральном размерном слое машины или меньше и естественно выровнена, не должно вызывать беспокойства. В частности, тип int всегда должен быть ОК в этом отношении, если он выровнен - это должно быть стандартным случаем для компилятора.
Относительно когерентности это потенциальная проблема в многопроцессорной системе. Вопрос в том, реализует ли система полную когерентность кэша или нет между процессорами. Если это реализовано, это обычно делается с протоколом MESI на аппаратном уровне. В вопросе не были указаны платформы, но как платформы Intel x86, так и платформы PowerPC являются кешами, согласованными между процессорами для нормально отображаемых областей данных программы. Поэтому этот тип проблемы не должен беспокоить обычные обращения к памяти данных между потоками, даже если есть несколько процессоров.
Последний вопрос относительно атомарности, который возникает, специфичен для атомарности чтения-модификации-записи. То есть, как вы гарантируете, что если значение будет считаться обновленным по значению и написанным, это произойдет атомарно, даже для процессоров, если их больше одного. Таким образом, для этого, чтобы работать без конкретных объектов синхронизации, потребовалось бы, чтобы все потенциальные потоки, обращающиеся к переменной, были читателями ТОЛЬКО, но ожидали, что только один поток может когда-либо быть писателем за один раз. Если это не так, вам нужен объект синхронизации, доступный для обеспечения атомарных действий над действиями read-modify-write для переменной.
Ответ 5
В вашем решении будет использоваться 100% процессор, среди прочих проблем. Google для "переменной условия".
Ответ 6
Крис Джет-Янг отметил, что:
Это работает только в модели памяти Java 1.5+. Стандарт С++ не предназначен для потоковой передачи, а волатильность не гарантирует согласованность памяти между процессорами. Для этого вам нужен барьер памяти
поэтому единственный верный ответ - это внедрение системы синхронизации, верно?
Ответ 7
Используйте ключевое слово volatile, чтобы указать компилятору, что значение может измениться в любое время.
volatile int myInteger;
Ответ 8
Нет, это не обязательно. Если вы объявляете переменную volatile, то, как правило, компилятор должен генерировать код, который всегда загружает переменную из памяти при чтении.
Ответ 9
Если область прав ( "extern", глобальная и т.д.), это изменение будет замечено. Вопрос в том, когда? И в каком порядке?
Проблема в том, что компилятор может и часто будет переупорядочить вашу логику, чтобы заполнить все параллельные конвейеры в качестве оптимизации производительности.
Это действительно не показано в вашем конкретном примере, потому что нет никаких других инструкций вокруг вашего назначения, но представьте функции, объявленные после того, как ваш bool присваивает выполнение до присваивание.
Отъезд Опасность трубопровода в википедию или поиск в Google для "переупорядочения команд компилятора"
Ответ 10
Как говорили другие, ключевое слово volatile
- ваш друг.: -)
Скорее всего, вы обнаружите, что ваш код будет работать, если у вас есть все опции оптимизации, отключенные в gcc. В этом случае (я считаю) он рассматривает все как изменчивые, и в результате переменная доступна в памяти для каждой операции.
При любом оптимизированном включении компилятор попытается использовать локальную копию, хранящуюся в регистре. В зависимости от ваших функций это может означать, что вы видите изменения в переменной с перерывами или, в худшем случае, никогда.
Использование ключевого слова volatile
указывает компилятору, что содержимое этой переменной может измениться в любое время и что оно не должно использовать локально кэшированную копию.
При всем этом вы можете найти лучшие результаты (как указано Jeff) с помощью семафора или переменной условия.
Это является разумным введением к теме.