itmo_conspects

Лекция 8. Упорядочивания в std::atomic и барьеры памяти

По умолчанию все операции над std::atomic в C++ выполняются с упорядочением memory_order_seq_cst. Это самый строгий и понятный режим: все потоки видят атомарные операции в едином глобальном порядке, как будто они выполняются строго одна за другой. Такой подход гарантирует наименьшее количество сюрпризов, но может ограничивать производительность из-за дополнительных барьеров памяти на аппаратном уровне

Всего режимов 4:


Барьеры памяти в C++ представлены функцией std::atomic_thread_fence. В отличие от упорядочения, привязанного к конкретной атомарной переменной, барьер действует как самостоятельная инструкция синхронизации для текущего потока. Есть два режима работы:

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

Пример с флагом, где данные не атомарны, но защищены барьером:

bool ready = false;
int data = 0;

// Поток 1
data = 42;
std::atomic_thread_fence(std::memory_order_release);
ready = true;

// Поток 2
if (ready) {
    std::atomic_thread_fence(std::memory_order_acquire);
    int v = data; // гарантированно 42
}

SPSC-очередь (Single Producer Single Consumer) - это свободная от блокировок структура данных для передачи сообщений между двумя потоками, где один только пишет, а второй только читает. Она строится на кольцевом буфере фиксированного размера и двух атомарных индексах: head (потребитель) и tail (производитель). Каждый поток модифицирует только свой индекс, поэтому для них достаточно memory_order_relaxed, но передача полезных данных требует синхронизации acquire-release

Производитель:

Потребитель:

Пример минимальной реализации на массиве:

template<typename T, size_t N>
class SPSCQueue {
    std::array<T, N> buffer;
    std::atomic<size_t> head{0}, tail{0};
public:
    bool push(const T& item) {
        size_t t = tail.load(std::memory_order_relaxed);
        size_t next = (t + 1) % N;
        if (next == head.load(std::memory_order_acquire)) return false; // буфер полон
        buffer[t] = item;
        tail.store(next, std::memory_order_release);
        return true;
    }

    bool pop(T& item) {
        size_t h = head.load(std::memory_order_relaxed);
        if (h == tail.load(std::memory_order_acquire)) return false; // буфер пуст
        item = buffer[h];
        head.store((h + 1) % N, std::memory_order_relaxed);
        return true;
    }
};

В этом коде проверка на пустоту/полноту и перемещение индексов используют минимально необходимые барьеры: acquire для чтения чужого индекса, release для публикации обновлённого индекса, relaxed для собственного индекса. Такой дизайн обеспечивает корректную синхронизацию данных без лишних накладных расходов