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

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

28 голосов
/ 23 сентября 2008

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

Тема 1:

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

Тема 2:

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

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

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

Ответы [ 10 ]

24 голосов
/ 23 сентября 2008

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

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

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

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

7 голосов
/ 23 сентября 2008

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

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;
  }
}
7 голосов
/ 23 сентября 2008

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

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

6 голосов
/ 23 сентября 2008

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

volatile int myInteger;

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

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

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

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

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

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

3 голосов
/ 23 сентября 2008

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

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

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

3 голосов
/ 23 сентября 2008

Ваше решение будет использовать 100% CPU, среди других проблем. Google для "условной переменной".

2 голосов
/ 23 сентября 2008

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

2 голосов
/ 23 сентября 2008

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

volatile int myInteger;
1 голос
/ 23 сентября 2008

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

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

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

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

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

Это является разумным введением в предмет.

1 голос
/ 23 сентября 2008

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

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

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

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

...