в чем разница между ++, add operation и fetch_add() в atom()

Я запускал следующий код много раз, но почему результат для приращения префикса, fetch_add() показывает правильный результат при операции добавления (+), он печатает неверный результат?

#include <iostream>
#include <mutex>
#include <future>
using namespace std;
atomic <int> cnt (0);
void fun()
{
    for(int i =0; i <10000000 ; ++i)
    {
       //++cnt; // print the correct result 20000000 
       //cnt = cnt+1; // print wrong result, arbitrary numbers 
       cnt.fetch_add(1); //  print the correct result 20000000 
    }
}
int main()
{
    auto fut1 = async(std::launch::async, fun);
    auto fut2 = async(std::launch::async, fun);
    fut1.get();
    fut2.get();
    cout << "value of cnt: "<<cnt <<endl;

} 

Ответы

Ответ 1

++cnt и cnt.fetch_add(1) - действительно атомные операции. Один поток блокируется, а другой поток читает, увеличивает и обновляет значение. Таким образом, эти два потока не могут наступать друг на друга. Доступ к cnt полностью сериализуется, и конечный результат, как и следовало ожидать.

cnt = cnt+1; не является полностью атомным. Он включает в себя три отдельные операции, только две из которых являются атомарными, но нет. К тому времени, когда поток атомарно считывает текущее значение cnt и делает его локальным, другой поток больше не блокируется и может свободно изменять cnt по желанию, пока эта копия увеличивается. Затем назначение инкрементированной копии обратно в cnt выполняется атомарно, но будет назначать устаревшее значение, если cnt уже был изменен другим потоком. Таким образом, конечный результат случайный, а не то, что вы ожидаете.

Ответ 2

cnt = cnt+1

Это не атомная операция. Сначала он загружает cnt в одну атомную операцию, затем добавляет и, наконец, сохраняет результат в другой атомной операции. Тем не менее, значение может быть изменено после загрузки, которое может быть перезаписано конечным хранилищем, что приводит к неверному конечному результату.

Остальные два являются атомными операциями и, таким образом, избегают такого состояния гонки.

Заметим, что оператор ++, --, +=, -=, &=, |=, ^= перегружен в std::atomic для обеспечения атомных операций.

Ответ 3

оператор ++ - это не одна операция, но 3 операции загружают хранилище, и, например, для однократной загрузки или хранения на arm64 не генерируют никаких данных, барьер данных. for ex atomic_add 1 - это куча кода с семантикой aquire/release

.LBB2_1:            
ldaxr   x8, [x0] //load exclusive register with aquire 
add x8, x8, #1  
stlxr   w9, x8, [x0] //store with rlease
cbnz    w9, .LBB2_1 //if another thread changed value, try again

где оператор ++ будет вызывать состояние гонки, если имитационно используется 2 потока

ldr x8, [x0]
add x8, x8, #1              // =1
str x8, [x0]