Многопоточное чтение, запись, выравнивание
У меня есть следующая программа.
class A {
struct {
int d1;
int d2;
} m_d;
int onTimer() {
return m_d.d1 + m_d.d2;
}
void update(int d1, int d2) {
m_d.d1 = d1;
m_d.d2 = d2;
}
};
A::update
а также A::onTimer
вызываются двумя разными потоками. При условии, что
- платформа x64
- каждый раз
onTimer
вызывается, результат должен быть таким, чтобы сумма рассчитывалась с использованием самых последних значенийm_d.d1
а такжеm_d.d2
вместо кэшированных значений - хорошо если
onTimer
называется во времяupdate
что сумма рассчитывается с обновленнойm_d.d1
и старыйm_d.d2
, - объект класса естественно выровнен
- не беспокойтесь о переупорядочении
- скорость критична
тогда мне нужно сделать любое из следующего
- использование
volatile
ключевое слово, так чтоm_d.d1
а такжеm_d.d2
не хранятся в кеше. - использовать любые замки
5 ответов
Компилятор может изменить порядок кода, а также процессор может изменить порядок операций чтения и сохранения. Если вас не волнует, что иногда m_d.d1 и m_d.d2 будут значениями из различных вызовов update(), тогда вам не нужно блокировать. Поймите, это означает, что вы можете получить старый m_d.d1 и новый m_d.d2, или наоборот. Порядок кода в потоке, устанавливающем значения, не контролирует порядок, в котором другой поток видит изменение значений. Вы сказали "5) не беспокойтесь о переупорядочении", поэтому я говорю, что блокировка не требуется.
На x86 int mov является "атомарным" в том смысле, что другой поток, читающий то же самое int, увидит предыдущее или новое значение, но не какой-то случайный набор битов. Это означает, что m_d.d1 всегда будет d1, который был передан в update(), и то же самое относится и к m_d.d2.
volatile указывает компилятору не использовать кэшированную копию значения (в регистре). Возможно, вам понадобится volatile, если у вас был цикл, который продолжал пытаться добавить эти значения, пока они изменялись другим потоком.
void func {
// smart optimizing compiler might move d1 into AX and d2 into BX here,
// OUTSIDE the loop, because the compiler doesn't see anything in
// the loop changing d1 or d2.
// The compiler does this because it saves 2 moves per iteration.
// This is referred to as "caching values in registers"
// by laymen like me.
while (1) {
printf("%d", m_d.d1 + m_d.d2); // might be using same initially
// "cached" AX, BX every iteration
}
}
Это не так в вашем примере, так как у вас есть вызов функции, который добавляет их (если функция не была встроена). При вызове функции не будет кэшироваться никаких значений в регистрах, поэтому она должна получать копию из памяти. Я полагаю, если вы хотите быть действительно супер уверены, что ничто никогда не кэшируется, вы можете сделать что-то вроде этого:
int onTimer() {
auto p = (volatile A*)this;
return p->m_d.d1 + p->m_d.d2;
}
Потому что вы упоминаете, что это нормально, если onTimer
отмечает частично обновленный m_d
Вам не нужен мьютекс, охраняющий весь объект. Тем не менее, C++ не дает никаких гарантий об атомарности int
, Для максимальной мобильности и правильности, вы должны использовать атомныйint
, Атомарные операции позволяют вам указать порядок памяти, который объявляет, какие гарантии вам нужны. Потому что вы говорите, что очень важно, чтобы onTimer
не используйте кэшированные значения, я бы порекомендовал вам использовать "Release-Acquire ordering". Это менее строго, чем порядок по умолчанию, используемый std::atomic
, но это все, что вам нужно здесь:
Если элементарный магазин в потоке A помечен
memory_order_release
и атомная нагрузка в потоке B из той же переменной помеченаmemory_order_acquire
все записи памяти (не атомарные и расслабленные атомарные), которые произошли - до того, как атомарное хранилище с точки зрения потока A, стало видимым побочным эффектом в потоке B, то есть, как только атомная загрузка завершена, поток B гарантированно видеть все темы, записанные в памяти.
Используя приведенное выше руководство, ваш код может выглядеть примерно так: Обратите внимание, что вы не можете использовать operator T()
преобразование atomic_int
потому что это эквивалентно load()
, который по умолчанию std::memory_order_seq_cst
заказ, который является слишком строгим для ваших нужд.
class A {
struct {
std::atomic_int d1;
std::atomic_int d2;
} m_d;
int onTimer() {
return m_d.d1.load(std::memory_order_acquire) +
m_d.d2.load(std::memory_order_acquire);
}
void update(int d1, int d2) {
m_d.d1.store(d1, std::memory_order_release);
m_d.d2.store(d2, std::memory_order_release);
}
};
Обратите внимание, что в вашем случае это упорядочение должно быть бесплатным (x86_64), но выполнение здесь должной осмотрительности поможет переносимости и устранит нежелательные оптимизации компилятора:
В строго упорядоченных системах (x86, SPARC TSO, мэйнфрейм IBM) упорядочение при получении релизов является автоматическим для большинства операций. Никаких дополнительных инструкций ЦП для этого режима синхронизации не выдается, затрагиваются только некоторые оптимизации компилятора (например, компилятору запрещено перемещать неатомарные хранилища за атомарным выпуском хранилища или выполнять неатомарную загрузку раньше, чем атомарное получение нагрузки). В слабо упорядоченных системах (ARM, Itanium, PowerPC) должны использоваться специальные инструкции по загрузке процессора или ограничению памяти.
В твоем случае я не думаю, что тебе нужен замок. Если вы не используете встроенную функцию, возможно, volatile тоже не нужна.
Единственный практический ответ здесь std::mutex
,
Также есть библиотека атомарных операций. Учитывая условие 3, возможно, вам не помешает пара атомных элементов. Тем не менее, я бы порекомендовал старомодный объект, защищенный мьютексом. Меньше сюрпризов.
Насколько я могу сказать. У вас есть только 1 поток для изменения данных. И вам не нужно изменять оба m_d.d1 и m_d.d2, чтобы быть атомарной операцией. Таким образом, нет необходимости использовать какой-либо замок.
Если у вас есть 2 или более потоков для обновления данных, и новое значение связано с предыдущим значением, вы можете использовать std::atomic<>
чтобы защитить это.
Если вам нужно, чтобы обновление 2 или более данных стало атомарной операцией, используйте std::mutex
чтобы защитить их.