Перейти к содержимому
Теория · Многопоточность: обзор · урок 8 из 12

std::atomic

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

Мьютекс защищает любой участок кода, но для одного числа он избыточен: перед каждым изменением поток захватывает мьютекс, после — освобождает, а остальные потоки ждут. Для простых переменных в заголовке <atomic> есть шаблон std::atomic — тот самый счётчик с кнопкой:

C++
#include <atomic>
#include <iostream>
#include <thread>

int main() {
    std::atomic<int> hits{0};
    std::thread worker([&hits]() { hits += 5; });
    hits += 2;
    worker.join();
    std::cout << hits << '\n';  // 7
}

Оба потока меняют hits одновременно, и мьютекса в программе нет. Но гонки данных не возникает: итог всегда равен 7.

Атомарная операция

std::atomic<int> хранит обычный int, но каждое действие с ним выполняет целиком. Это касается таких действий:

  • ++ и --;
  • += и -=;
  • чтение значения;
  • присваивание.

Это атомарные операции. Другой поток видит такую операцию либо ещё не начатой, либо уже законченной, но никогда не выполненной наполовину. Поэтому одновременные изменения не теряются: три шага «прочитать, прибавить, записать» сливаются в одно нажатие кнопки.

Проверьте: в какую бы очередь вы ни нажимали «Шаг A» и «Шаг B», итог всегда на два больше начального.

ВживуюДва потока, один атомарный ++countercounter = 5
общая переменная
5
counter
поток A
  1. ++counter целикомlock xadd [counter], eax
поток B
  1. ++counter целикомlock xadd [counter], eax
std::atomic<int>: ++counter — одна неделимая инструкция (clang, x86-64)

counter = 5, но теперь это std::atomic<int>: ++counter — один неделимый шаг. Попробуйте потерять увеличение.

Порядок шагов выбираете вы

Счётчик из урока о гонке данных станет верным, если заменить int counter = 0; на std::atomic<int> counter{0};. Мьютекс при этом не нужен, и программа всегда печатает 200000.

std::atomic подходит и для других простых типов, например bool. Переменную типа std::atomic<…> называют атомарной. Флаг остановки обычно объявляют как std::atomic<bool> stop{false};. Один поток присваивает stop = true, а другой проверяет stop в цикле и выходит из него, когда флаг выставлен.

Атомарная переменная или мьютекс

Здесь аналогия со счётчиком заканчивается. Кнопка защищает одно число и одно нажатие, а не цепочку действий.

Атомарная переменная защищает одно значение и одну операцию над ним. Мьютекс защищает группу действий:

  • согласованное изменение двух переменных;
  • проверку значения вместе с его изменением.

Две отдельные атомарные переменные здесь не помогут: между двумя операциями другой поток может увидеть промежуточное состояние.

Для одного счётчика или флага атомарная переменная обычно дешевле мьютекса. Для всего остального, где действий больше одного, выбирают мьютекс.

Частая ошибка

Атомарную переменную увеличивают в два шага:

C++
counter = counter + 1;  // ✗

Гонки данных здесь нет: и чтение counter, и запись атомарны. Но это две отдельные операции, и между ними другой поток может изменить counter. Тогда увеличение другого потока потеряется, и итог может оказаться меньше 200000. Пишите одной операцией: ++counter или counter += 1.

Главное

  • std::atomic<int> выполняет ++, +=, чтение и присваивание целиком, поэтому одновременные изменения не теряются.
  • Для одного счётчика или флага атомарная переменная обычно дешевле мьютекса.
  • Группу действий над несколькими значениями защищает мьютекс, а не несколько атомарных переменных.
  • counter = counter + 1 — две атомарные операции, а не одна: увеличение может потеряться.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом