Давайте обманем атомик. Вот две функции - можно ли, общаясь к атомику только с их помощью, увидеть некорректное состояние ?

void write(std::atomic<int64_t>& x, int64_t v) {
    x.store(v, std::memory_order_seq_cst);
}
int64_t read(std::atomic<int64_t>& x) {
    return x.load(std::memory_order_seq_cst);
}

Пишем тест:

  • 1-ый тред упорно пишет -1 в атомик (то есть все биты равны единице)

  • 2-ой тред пишет 2 (то есть ровно 1 бит - второй младший равен единице)

  • 3-ий тред читает и стопает программу, если прочитает нечётное отрицательное число

void test(std::atomic<int64_t>& x) {
    constexpr size_t ITERS = 100'000'000;
    std::thread writePart1([&x]() {
        for(size_t i = 0; i < ITERS; ++i) {
            write(x, -1);
        }
    });
    std::thread writePart2([&x]() {
        for(size_t i = 0; i < ITERS; ++i) {
            write(x, 2);
        }
    });
    std::thread reader([&x]() {
        for(size_t i = 0; i < ITERS; ++i) {
            int64_t r = read(x);
            if ((r & 1) == 0 && r < 0) {
                std::cout << "found non atomic behaviour, iter=" << i/1e6 << "m" << std::endl;
                exit(0);
                return;
            }
        }
    });
    writePart1.join();
    writePart2.join();
    reader.join();
    std::cout << "finish, not found non atomic behaviour" << std::endl;
}

И, с помощью легко пропускаемой ошибки, эта программа у меня стабильно падает где-то за 7-10млн итераций чтения

+ clang++ -std=c++20 main.cpp -o nonatomic.exe -Wall -O2 -DNDEBUG
+ ./nonatomic.exe
found non atomic behaviour, iter=8.15546m

И вот, пора переходить к разгадке

int main(int argc, const char* _ []) {
    constexpr size_t alignment = 4096;
    char* buf [alignment * 3];
    size_t ptr = size_t(buf) / alignment * alignment + alignment - 4;
    std::atomic<int64_t>* nonAligned = new((void*)ptr) std::atomic<int64_t>(0);
    std::atomic<int64_t> aligned;
    std::atomic<int64_t>& x = (argc == 1) ? *nonAligned : aligned;

    test(x);
    return 0;
}

Если не совершать ошибку (запустится с любым аргументом) то не промежуточного стейта мы не увидим

+ ./nonatomic.exe aligned
finish, not found non atomic behaviour

Думаю уже понятно, что происходит - мы располагаем наш атомик на 8 байт по середине двух страниц.
В частности - мы располагаем старшие и младшие 4 биты в разных кеш-линиях
А алгоритмы синхронизации в процессоре между ядрами работают только в рамках кеш-линий.

Что же конкретное мы нарушили? Мы нарушили выравнивание объекта. alignof(std::atomic<int64_t>) == 8, а нарушение алайнмента - UB.
Как же посадить такую ошибку? Я точно видел такую ошибку в районе попытки реализовывать что-то вроде арен или memory-pool-ов.
Выделяют память, выдают её кусками, а про выравнивание при выделении забыли (это встроенные new сделают как надо, ибо знают тип, а malloc или самописные функции получения буфера из мемори-пула, этого не делают)
UPD: malloc возвращает максимально выровненный адрес во всех нормальных реализациях

Что ещё интересного можно заметить? Давайте посмотрим в godbold
https://godbolt.org/z/j5jP8ozEb

  1. сами операции чтения и записи не требуют выравнивания (мы не получаем sigill, хотя например с simd инструкциями это легко случается даже на x86)

  2. изменение memory-order ничего не меняет (видимо, можно попытаться заявить, что в случае x86 memory-order аргументы прежде всего описывают ограничений перестановки инструкций компилятором, но не порождают дополнительных явных инструкций (сброса буферов чтения/записи например))

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

Комментарии (10)


  1. unreal_undead2
    20.02.2026 06:36

    эта программа у меня стабильно падает где-то за 7-10млн итераций чтения

    У меня она стабильно сразу выдаёт

    Bus error (core dumped)

    Ваша x86 архитектура позволяет слишком много вольностей )

    PS -fsanitize=undefined явно детектит проблему и в gcc и в clang.


    1. ilnurKh Автор
      20.02.2026 06:36

      а в режиме бин трансляции если таргет платформа была x86 что будет?

      емнип, ошибку при невыровненном чтении на x86 тоже можно получить, если в какую-нибудь avx инструкцию, требующую выравнивание, подать невыровненный адрес

      А так, да - дебажить bus-error проще чем что-то стреляющее с такой малой вероятностью (и космические лучи обвинишь, ища разгадку или репродьюс)


      1. unreal_undead2
        20.02.2026 06:36

        а в режиме бин трансляции

        Тут уже от транслятора зависит, сконвертирует он один атомик в другой или навернёт логику с локами. Под рукой ничего нет чтобы быстро проверить.

        Да, SIMD на x86 часто требует выравнивания (хотя есть, скажем, movups).

        дебажить bus-error проще

        Согласен. Но санитайзер тоже порадовал, надо бы почаще использовать...


        1. ilnurKh Автор
          20.02.2026 06:36

          ubsan ловит, да

          у себя ubsan чёт не удаётся завести из-за того что очень много в зависимостях надо пограничного вычищать, а ещё очень долгий

          но мысли сделать хотя бы "раз в неделю" запуски приходят


  1. viordash
    20.02.2026 06:36

    хм, напоминает старый анекдот про японскую бензопилу и суровых лесорубов


  1. kibb
    20.02.2026 06:36

    Есть бит RFLAGS.AC, который вызывает исключение #AC при невыравненном доступе. Им пользоваться невозможно, потому что почти все компиляторы (не только C) генерируют код, нарушающий это правило.

    В сравнительно новых интеловских процессорах появился MSR_MEMORY_CTRL.SPLIT_LOCK_DISABLE, разрешение которого приводит к #AC при невыравненных locked доступах.

    В вашем примере это бы сработало из-за иначе ненужного seq-cst режима. Если бы использовался relaxed или acq/rel, то на x86 они обычно моделируются простыми load и store, и split lock detection бы не сработал.

    P.S. Явление называется 'torn writes' и в дикой природе встречается начиная с Core2 (~2008 г).


    1. ilnurKh Автор
      20.02.2026 06:36

      Генерируемый асемблер в годболте приложенном не меняется при смене мемори ордеров


      1. kibb
        20.02.2026 06:36

        Для load'ов да, plain load будет seq-cst если store с lock-префиксом. gcc генерирует XCHG для записи, эта инструкция с неявным префиксом lock.

        Но собственно, это ничего в моем ответе не меняет.


  1. black_warlock_iv
    20.02.2026 06:36

    а malloc <...> этого не делают

    Делает. Он возвращает всегда максимально выровненный указатель.


    1. ilnurKh Автор
      20.02.2026 06:36

      Спасибо за уточнение. Добавил апдейт в текст