Этот алгоритм блокировки может выйти из строя из-за буферов хранилища в процессорах Intel: хранилища не попадают непосредственно в кэш 1-го уровня, а на некоторое время ставятся в очередь в буфере хранилища и, следовательно, невидимы для другого процессора в течение этого времени:
Для оптимизации производительности выполнения команд архитектура IA-32 допускает отклонения от модели строгого упорядочения, называемой упорядочением процессоров в процессорах семейства Pentium 4, Intel Xeon и P6. Эти вариации упорядочения процессора (называемые здесь моделью упорядочения памяти) позволяют операциям повышения производительности, таким как чтение, опережать буферизованные записи. Целью любого из этих вариантов является увеличение скорости выполнения команд, в то время какподдержание согласованности памяти даже в многопроцессорных системах.
Буферы хранилища необходимо очистить, чтобы эта блокировка работала, используя std::memory_order_seq_cst
для хранилищ для блокировок (по умолчаниюПорядок памяти для загрузок и хранилищ можно просто сделать s_lock1 = 1;
, например).std::memory_order_seq_cst
для хранилищ приводит к тому, что компилятор генерирует xchg
инструкцию или вставку mfence
инструкцию после хранилища, оба из которых делают эффект хранилища видимым для других процессоров:
Атомные операции помечены memory_order_seq_cst
не только упорядочивает память так же, как упорядочение при отпускании / получении (все, что происходило до того, как хранилище в одном потоке становится видимым побочным эффектом в потоке, который выполнил загрузку), но также устанавливает единый общий порядок изменениявсех атомарных операций, помеченных .Последовательное упорядочение может быть необходимо для ситуаций с несколькими производителями и несколькими потребителями, когда все потребители должны наблюдать за действиями всех производителей, происходящими в одном и том же порядке.Полное последовательное упорядочение требует полной инструкции процессора CPU для всех многоядерных систем.Это может стать узким местом в производительности, поскольку оно заставляет затронутые обращения к памяти распространяться на каждое ядро.
Рабочий пример:
std::atomic<unsigned> s_lock1{0};
std::atomic<unsigned> s_lock2{0};
std::atomic<unsigned> s_var{0};
void func1() {
while(true) {
s_lock1.store(1, std::memory_order_seq_cst);
if(s_lock2.load(std::memory_order_seq_cst) != 0) {
s_lock1.store(0, std::memory_order_seq_cst);
continue;
}
if(s_var.load(std::memory_order_relaxed) > 0) {
printf("bad\n");
}
usleep(1000);
s_lock1.store(0, std::memory_order_seq_cst);
}
}
void func2() {
while(true) {
s_lock2.store(1, std::memory_order_seq_cst);
if(s_lock1.load(std::memory_order_seq_cst) != 0) {
s_lock2.store(0, std::memory_order_seq_cst);
continue;
}
s_var.store(1, std::memory_order_relaxed);
usleep(5000);
s_var.store(0, std::memory_order_relaxed);
s_lock2.store(0, std::memory_order_seq_cst);
}
}
int main() {
std::thread t1(func1);
std::thread t2(func2);
t1.join();
t2.join();
}