Каким должен быть порядок памяти для последовательной загрузки атома, когда допустимы определенные ошибки
Предположим, что пользователь крутит ручку на MIDI-контроллере, и значения отправляются в мою программу в виде приращений и приращений к сохраненному значению. Поворот ручки в одну сторону пошлет серию уменьшений, их значение зависит от скорости вращения; скручивание приращений в другую сторону. Я хочу, чтобы сохраненное значение (и значение, испускаемое следующей функцией) находилось в диапазоне от 0 до 100. Если одно или несколько сообщений будут отброшены, это не страшно, но я не хочу, чтобы неожиданные серьезные изменения в значение, испускаемое OffsetResult_
функция.
Мой вопрос тогда - следующие директивы порядка памяти выглядят правильно? Самым ясным для меня является compare_exchange_strong
, Программа использует это как store
это может привести к сбою, поэтому кажется, что применяется порядок освобождения памяти.
Могу ли я даже пойти в std::memory_order_relaxed
так как основная проблема - просто атомарность изменений в StorageV, а не запоминание каждого изменения в StoreV?
Есть ли общий способ взглянуть на комбинированные функции загрузки / сохранения, чтобы увидеть, должны ли они быть получены, освобождены или последовательно согласованы?
class ChannelModel {
ChannelModel():currentV{0}{};
int OffsetResult_(int diff) noexcept;
private:
atomic<int> storedV;
};
int ChannelModel::OffsetResult_(int diff) noexcept {
int currentV = storedV.fetch_add(diff, std::memory_order_acquire) + diff;
if (currentV < 0) {//fix storedV unless another thread has already altered it
storedV.compare_exchange_strong(currentV, 0, std::memory_order_release, std::memory_order_relaxed);
return 0;
}
if (currentV > 100) {//fix storedV unless another thread has already altered it
storedV.compare_exchange_strong(currentV, 100, std::memory_order_release, std::memory_order_relaxed);
return 100;
}
return currentV;
}
Обратите внимание, что реальный код намного сложнее, и разумно полагать, что ответ на каждое сообщение от контроллера займет достаточно много времени, и иногда эта функция будет вызываться двумя потоками практически одновременно.
1 ответ
Я сделаю предположение, что currentV
является локальной переменной в OffsetResult_
, По какой-то причине он инициализируется в конструкторе класса, но не определяется как переменная класса.
Вы меняете значение storedV
с fetch_add
а затем скорректировать возможные ошибки с compare_exchange_strong
, Это не правильно... compare_exchange_strong
используется здесь как условный store
, Только если другой поток не изменяет значение, storedV
будет обновлено.
Порядок памяти, который вы указываете, неправильный. В общем, release
упорядочение используется с атомным store
чтобы указать, что данные "выпущены", т.е. сделано доступным для другой темы, которая будет load
из того же атомного использования acquire
упорядоченность. release
а также acquire
Заказ формируется во время выполнения отношений и всегда приходит парами. Это отношение отсутствует в вашем коде, когда currentV
находится в заданном диапазоне, так как вы никогда не выполняете release
операция.
Не понятно, почему вы хотите указать заказы. Обратите внимание, что вам не нужно устанавливать порядок в памяти, в этом случае (безопаснее) по умолчанию (std::memory_order_seq_cst
) будет использоваться. Правильность более слабого порядка зависит от данных, которые он синхронизирует между потоками. Без зависимости от данных, используя std::memory_order_relaxed
может быть правильным, но этот контекст отсутствует в коде. Однако, поскольку атомарный элемент связан со значением ручки, вероятно, что поворот ручки приведет к некоторым действиям, связанным с другими данными. Я не буду пытаться оптимизировать с более слабыми упорядочениями памяти здесь. Скорее всего, не будет никакой выгоды, так как вызов Read-Modify-Write (compare_exchange_x
) уже относительно дорого. Кроме того, если при использовании более слабого упорядочения памяти возникает ошибка, отладку будет очень сложно.
Ты можешь использовать std::compare_exchange_weak
для настройки без потери обновлений:
int ChannelModel::OffsetResult_(int diff) noexcept {
int updatedV;
int currentV = storedV.load();
do {
updatedV = currentV + diff;
if (updatedV > 100)
updatedV = 100;
else if (updatedV < 0)
updatedV = 0;
} while (!storedV.compare_exchange_weak(currentV, updatedV));
return updatedV;
}
Ключ в том, что compare_exchange_weak
будет только (атомарно) обновлять storedV
если он все еще (или снова) равен currentV
, Если эта проверка не пройдена, она будет повторять цикл снова. Используется в цикле, compare_exchange_weak
(который может неуспешно провалиться) является лучшим выбором, чем compare_exchange_strong
,
Упорядочение памяти - сложная тема, вот хороший обзор.