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

Предположим, что пользователь крутит ручку на 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,

Упорядочение памяти - сложная тема, вот хороший обзор.

Другие вопросы по тегам