С++ 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 миллионов.
Что мне здесь не хватает?
Ответы
Ответ 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()
в окна в тело цикла.