Подтвердить что ты не робот

Как атомизировать отрицание std:: atomic_bool?

Наивное булево отрицание

std::atomic_bool b;
b = !b;

не кажется атомарным. Я подозреваю, что это потому, что operator! запускает трансляцию в обычный bool. Как бы атомно выполнить эквивалентное отрицание? Следующий код иллюстрирует, что наивное отрицание не является атомарным:

#include <thread>
#include <vector>
#include <atomic>
#include <iostream>

typedef std::atomic_bool Bool;

void flipAHundredThousandTimes(Bool& foo) {
  for (size_t i = 0; i < 100000; ++i) {
    foo = !foo;
  }
}

// Launch nThreads std::threads. Each thread calls flipAHundredThousandTimes 
// on the same boolean
void launchThreads(Bool& foo, size_t nThreads) {

  std::vector<std::thread> threads;
  for (size_t i = 0; i < nThreads; ++i) {
    threads.emplace_back(flipAHundredThousandTimes, std::ref(foo));
  }

  for (auto& thread : threads) thread.join();

}

int main() {

  std::cout << std::boolalpha;
  Bool foo{true};

  // launch and join 10 threads, 20 times.
  for (int i = 0; i < 20; ++i) {
    launchThreads(foo, 10);
    std::cout << "Result (should be true): " << foo << "\n";
  }

}

Код запускает 10 потоков, каждый из которых переворачивает atom_bool larrge, даже, количество раз (100000) и выводит булев. Это повторяется 20 раз.

EDIT. Для тех, кто хочет запустить этот код, я использую моментальный снимок GCC 4.7 на ubuntu 11.10 с двумя ядрами. Возможны следующие варианты компиляции:

-std=c++0x -Wall -pedantic-errors -pthread
4b9b3361

Ответ 1

b = !b не является атомарным, потому что в источнике C++ вы читаете атомарное чтение b (эквивалентно b.load(), а затем отдельно атомарное присваивание b (эквивалентно b.store()).

Ничто не превращает всю комбинацию в атомарную операцию RMW в абстрактной машине C++, и нет никакого синтаксиса для объединения произвольных операций в атомарные операции RMW (кроме помещения его в цикл повторения CAS).


Есть два варианта использования:

  1. Вместо atomic<bool>, используйте целочисленный тип (например, atomic<int> или atomic<unsigned char>), который может быть 0 или 1, и xor его с 1:

    std::atomic<int> flag(0);
    
    flag ^= 1;        //equivalent to flag.fetch_xor(1);
    

    К сожалению, fetch_xor не предоставляется для atomic<bool>, только для целочисленных типов.

  2. Выполняйте операцию сравнения/обмена в цикле, пока она не завершится успешно:

    std::atomic<bool> flag(false);
    
    bool oldValue = flag.load();
    while (!flag.compare_exchange_weak(oldValue, !oldValue)) {}
    

    К сожалению, компиляторы для x86 обычно не оптимизируют этот цикл в
    lock xor byte [flag], 1 в asm; вы получите реальный цикл повторения cmpxchg. На практике повторные циклы cmpxchg хороши с низкой конкуренцией. В худшем случае это не без ожидания, но без блокировки, потому что по крайней мере один поток будет прогрессировать каждый раз, когда все они повторяются. (На практике это более сложно с аппаратным арбитражем, для которого ядро даже получает доступ к строке кэша, чтобы попытаться.)

    Если возможна высокая конкуренция, предпочтите целочисленную версию, которая позволяет использовать атомарный xor.