многопоточность — безопасно ли использовать volatile bool, чтобы заставить другой поток ждать? (C ++)

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

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

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

Для этого у меня есть рендер, который бесконечно работает в своей ветке:

volatile bool stillRendering;

void RenderThreadFunction()
{
stillRendering = true;

while(programRunning)
{
renderer->render();
}

stillRendering = false;
}

В главном потоке программы, когда получено сообщение о выходе windproc, я делаю:

void OnQuit()
{
programRunning = false;
while(stillRendering)
{
}

delete application;
}

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

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

Добавление volatile к just stillRendering приводило к успешному завершению работы приложения при каждом его тестировании. Я не уверен, почему не имеет значения, является ли «programRunning» изменчивым.

Наконец, я не уверен, как повлияет на производительность программы использование volatile для «stillRendering». Для меня не имеет значения, влияет ли создание stillRendering volatile на производительность OnQuit (), но для меня имеет значение, влияет ли это на производительность RenderThreadFunction ()

8

Решение

Это совершенно небезопасно, хотя с некоторыми
компиляторы. В принципе, volatile влияет только на переменную это
прилагается, так RendererThreadFunction, например, мог установить
stillRendering ложный до закончив
renderer->render();, (Это правда, даже если оба
stillRendering а также programRunning оба были нестабильны.)
Вероятность проблемы очень мала, поэтому, вероятно, тестирование
не раскроет это. И, наконец, некоторые версии VC ++ делать дать
volatile семантика атомарного доступа под C ++ 11, в
В этом случае ваш код будет работать. (Пока вы не скомпилируете с
другая версия VC ++, конечно.)

При условии renderer->render() почти наверняка занимает
немалое количество времени, нет абсолютно никаких причин
для того, чтобы не использовать условную переменную здесь. О единственном времени
вы бы использовали volatile для такого рода вещи, если выключение
механизм был вызван сигналом (в этом случае, тип
было бы sig_atomic_t, и не boolхотя на практике
это, вероятно, не имеет никакого значения). В этом случае там
не было бы двух потоков, но только поток рендерера и
обработчик сигнала.

8

Другие решения

Если вы хотите, чтобы ваш код работал на всех архитектурах во всех компиляторах, используйте атомарный код C ++ 11:

std::atomic<bool> stillRendering;

void RenderThreadFunction()
{
stillRendering = true;

while(programRunning)
{
renderer->render();
}

stillRendering = false;
}

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

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

Атомика C ++ 11 позволяет избежать обеих этих проблем.

ОК, так что это в основном предназначалось для исправления вашего текущего кода и предупреждения о неправильном использовании volatile, Предложение Джеймса об использовании условной переменной (которая является просто более эффективной версией того, что вы делаете), вероятно, является лучшим реальным решением для вас.

6

Есть три проблемы, которые решает атомная схема C ++ 11.

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

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

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

Загрузка и сохранение в атомарной переменной (с упорядочением памяти по умолчанию) предотвращает все три проблемы. Маркировка переменной как volatile не.

РЕДАКТИРОВАТЬ: не пытайтесь выяснить, какие архитектуры создают какие проблемы. Автор стандартной библиотеки уже сделал это для архитектуры, для которой предназначена реализация библиотеки. Не ищите ярлыки; просто используйте атомику. Вы ничего не потеряете.

4

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

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

Я предполагаю что renderer->render() занимает много времени, поэтому чтение stillRendering не должен сильно влиять на общее время выполнения. volatile как правило, просто означает «пожалуйста, не вносите это в реестр и храните его там».

(Вам, вероятно, нужно programRunning быть volatile тоже!)

2

Добавление volatile к just stillRendering приводило к успешному завершению работы приложения при каждом его тестировании.

Да, ваш сценарий будет работать.

Распространенная ошибка, возникающая при использовании volatile переменные для синхронизации потоков — это когда операции над volatile переменные предположительно атомный. Это не так.

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

Я не уверен, почему не имеет значения, является ли «programRunning» изменчивым.

Это имеет значение. Сделай это volatile,

Создание переменной volatile гарантирует, что вы избежите определенных оптимизаций кеша, чего вы и хотите.

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

Наконец, я не уверен, как повлияет на производительность программы использование volatile для «stillRendering».

Это может негативно повлиять на вашу производительность:

while(stillRendering)
{
}

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

Подумайте о добавлении режима ожидания в цикл while.

1
По вопросам рекламы [email protected]