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

С++ 11 Реализация Spinlock с использованием <atomic>

Я внедрил класс SpinLock, как следует

struct Node {
    int number;
    std::atomic_bool latch;

    void add() {
        lock();
        number++;
        unlock();
    }
    void lock() {
        bool unlatched = false;
        while(!latch.compare_exchange_weak(unlatched, true, std::memory_order_acquire));
    }
    void unlock() {
        latch.store(false , std::memory_order_release);
    }
};

Я реализовал выше класс и сделал два потока, которые вызывают метод add() одного и того же экземпляра класса Node 10 миллионов раз за поток.

результат, к сожалению, не составляет 20 миллионов. Что мне здесь не хватает?

4b9b3361

Ответ 1

Проблема заключается в том, что compare_exchange_weak обновляет переменную unlatched после ее отказа. Из документации compare_exchange_weak:

Сравнивает содержимое содержащегося атома с Ожидаемый результат: - если true, он заменяет содержащееся значение значением val (например, store). - если false, он заменяет ожидаемое с содержащимся значением.

I.e., после первого отказа compare_exchange_weak, unlatched будет обновлено до true, поэтому следующая итерация цикла попытается compare_exchange_weak true с true. Это удается, и вы просто взяли блокировку, удерживаемую другим потоком.

Решение: Обязательно установите unlatched назад на false перед каждым compare_exchange_weak, например:

while(!latch.compare_exchange_weak(unlatched, true, std::memory_order_acquire)) {
    unlatched = false;
}

Ответ 2

Как упоминалось в @gexicide, проблема в том, что функции compare_exchange обновляют переменную expected с текущим значением атомной переменной, что и является причиной, почему вы должны использовать локальную переменную unlatched в первое место. Чтобы решить эту проблему, вы можете установить unlatched обратно на false в каждой итерации цикла.

Однако вместо использования compare_exchange для чего-то его интерфейс довольно плохо подходит, гораздо проще использовать std::atomic_flag вместо:

class SpinLock {
    std::atomic_flag locked = ATOMIC_FLAG_INIT ;
public:
    void lock() {
        while (locked.test_and_set(std::memory_order_acquire)) { ; }
    }
    void unlock() {
        locked.clear(std::memory_order_release);
    }
};

Источник: cppreference

Вручную указать порядок памяти - это лишь незначительная потенциальная настройка производительности, которую я скопировал из источника. Если простота важнее последнего бит производительности, вы можете придерживаться значений по умолчанию и просто вызвать locked.test_and_set() / locked.clear().

Btw.: std::atomic_flag - единственный тип, который гарантированно свободен от блокировки, хотя я не знаю никакой платформы, где oparations на std::atomic_bool не блокируются.

Обновление: Как объяснялось в комментариях @David Schwartz, @Anton и EmpireTechnik Empire, пустая петля имеет некоторые нежелательные эффекты, такие как предсказание ветки, потоковое голодание на HT-процессорах и чрезмерно высокое энергопотребление - Короче говоря, это довольно неэффективный способ подождать. Эффект и решение - это архитектура, платформа и приложение. Я не эксперт, но обычным решением, похоже, является добавление либо к cpu_relax() в linux, либо YieldProcessor() в окна в тело цикла.