Почему этот многопоточный счётчик неверен и как это исправить?
counter уже имеет тип std::atomic<int>, но после того как несколько std::thread выполнили worker() и для каждого вызван join, итоговый counter слишком мал.
std::atomic<int> counter{0};
void worker() { for (int i = 0; i < 100000; ++i) counter = counter + 1; }
// запускаем worker() в нескольких std::thread, затем join всехНайдите и исправьте ошибку.
counter = counter + 1 — атомарное чтение и отдельная атомарная запись: два потока могут прочитать одно значение, и инкремент теряется. UB нет, только потерянные обновления. Решение: ++counter или counter.fetch_add(1) — один атомарный шаг.
- Считать, что любое выражение с
std::atomicатомарно целиком, хотя атомарна только каждая отдельная операция над ним - Называть потерю обновлений у атомарной переменной неопределённым поведением, хотя программа корректна, а ошибка только в логике
- Винить порядок памяти, хотя
seq_cstпо умолчанию и так самый строгий, а ошибка в раздельных чтении и записи
- Почему для
fetch_addу счётчика, который читают только послеjoin, хватаетstd::memory_order_relaxed? - Когда обновлению нужен цикл с
compare_exchange_weakвместоfetch_add?
У std::atomic атомарна каждая отдельная операция, но не выражение целиком. counter = counter + 1 читает значение (load), прибавляет единицу к обычному временному значению и записывает результат (store). Между чтением и записью другой поток может успеть записать своё значение, и его инкремент затирается. Гонки данных нет, поведение определено, но итог меньше ожидаемого.
std::atomic<int> counter{0};
void worker() {
for (int i = 0; i < 100000; ++i) {
counter.fetch_add(1); // или ++counter, или counter += 1
}
}fetch_add выполняет чтение, сложение и запись как одну неделимую операцию, поэтому ни один инкремент не теряется. Мьютекс тоже исправил бы ошибку, но для одного счётчика он избыточен.