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

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, но каждое действие с ним выполняет целиком. Его ++, --, +=, -=, чтение и присваивание — атомарные операции (atomic operations). Другой поток видит такую операцию либо ещё не начатой, либо уже законченной, но никогда не выполненной наполовину. Поэтому одновременные изменения не теряются.

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

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

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

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

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

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

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

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

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