2017-01-30 11 views
2

Предположим, что пользователь скручивает ручку на MIDI-контроллере, и значения отправляются в мою программу с возрастанием и уменьшением до сохраненного значения. Скручивая ручку в одну сторону, вы отправите серию декрементов, их значение зависит от скорости вращения; скручивая другой шаг. Я хочу сохранить сохраненное значение (и значение, испускаемое следующей функцией), между 0 и 100. Если одно или несколько сообщений будут удалены, это не имеет большого значения, но я не хочу, чтобы неожиданные серьезные изменения в значение, испускаемое функцией OffsetResult_.Каким должен быть порядок памяти для последовательной атомарности при хранении при определенных ошибках?

Мой вопрос тогда есть - следующие директивы заказа памяти выглядят правильно? Самый ясный для меня - compare_exchange_strong. Программа использует это как store, который может выйти из строя, поэтому кажется, что порядок выпуска памяти памяти применяется.

Могу ли я даже пойти на std::memory_order_relaxed, поскольку основная проблема заключается в простоте изменений в храненииV, а не в запоминании каждого изменения в сохраненныхV?

Есть ли общий способ взглянуть на комбинированные функции загрузки/хранения, чтобы определить, следует ли его приобретать, выпускать или последовательно согласовывать?

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; 
} 

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

+0

1. 'fetch_add (...) + diff' добавляет' diff' дважды, не так ли? 2. Не будет ли это 'fetch_add' результатом' storedV = storedV + diff', который может быть '< 0' or '> 100', который, в свою очередь, может быть видимым для других потоков? – Pixelchemist

+0

fetch_add возвращает * исходное значение * и сохраняет * результат * добавления. Таким образом, чтобы иметь возможность правильно использовать результат в функции, не выполняя другую выборку для атома (и, возможно, имея проблемы параллелизма), я добавляю diff к вычисленному значению. – rsjaffe

+0

Да, значение out of bounds может быть видимым для других потоков, но это не является серьезной проблемой, так как функция никогда не вернет значение за пределами границ. Таким образом, пользователь не заметит проблемы, кроме, может быть, очень небольшой задержки при запуске контроллера в противоположном направлении. Моя цель состоит в том, чтобы каждый поток имел внутренне согласованное представление о значении, поэтому основная часть работы выполняется с помощью временной переменной. – rsjaffe

ответ

0

Я сделаю предположение, что 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.

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

+0

Я не беспокоюсь о потере обновлений, поэтому не нужно перейдите в цикл для compare_exchange. интересный комментарий о заказах памяти. С процессорами Intel, есть ли реальная польза для суеты вокруг с упорядочением памяти? – rsjaffe

+0

Чтобы исправить мой последний комментарий - я спрашиваю о заказе памяти для compare_exchange. Похоже, что это достаточно дорого, что изменения в упорядочении памяти не стоят того. Это верно? – rsjaffe

+0

@rsjaffe Это точно. RMW дорого, потому что он должен блокировать шину, чтобы синхронизировать с другими ядрами, обращающимися к данным. В вашем коде для 'fetch_add' и' compare_exchange_strong' вы увидите код объекта, например 'lock xadd' и' lock cmpxchg' соответственно. На X86 нет никакой разницы между тем, что вы используете, и 'seq_cst' – LWimsey