Простейший стандартный атомарный тип std::atomic_flag
представляет булевский флаг. Объекты этого типа могут находиться в одном из двух состояний: установлен или сброшен. Этот тип намеренно сделан максимально простым, рассчитанным только на применение в качестве строительного блока. Поэтому увидеть его в реальной программе можно лишь в очень специфических обстоятельствах. Тем не менее, он послужит нам отправной точкой для обсуждения других атомарных типов, потому что на его примере отчетливо видны общие относящиеся к ним стратегии.
Объект типа std::atomic_flag
должен быть инициализирован значением ATOMIC_FLAG_INIT
. При этом флаг оказывается в состоянии сброшен . Никакого выбора тут не предоставляется — флаг всегда должен начинать существование в сброшенном состоянии:
std::atomic_flag f = ATOMIC_FLAG_INIT;
Требование применяется вне зависимости от того, где и в какой области видимости объект объявляется. Это единственный атомарный тип, к инициализации которого предъявляется столь специфическое требование, зато при этом он является также единственным типом, гарантированно свободным от блокировок. Если у объекта std::atomic_flag
статический класс памяти, то он гарантированно инициализируется статически, и, значит, никаких проблем с порядком инициализации не будет — объект всегда оказывается инициализированным к моменту первой операции над флагом.
После инициализации с флагом можно проделать только три вещи: уничтожить, очистить или установить, одновременно получив предыдущее значение. Им соответствуют деструктор, функция-член clear()
и функция-член test_and_set()
. Для обеих функций clear()
и test_and_set()
можно задать упорядочение памяти. clear()
— операция сохранения , поэтому варианты упорядочения memory_order_acquire
и memory_order_acq_rel
к ней неприменимы, a test_and_set()
— операция чтения-модификации-записи, так что к ней применимы любые варианты упорядочения. Как и для любой атомарной операции, по умолчанию подразумевается упорядочение memory_order_seq_cst
. Например:
f.clear(std::memory_order_release);←
(1)
bool x = f.test_and_set(); ←
(2)
Здесь при вызове clear()
(1)явно запрашивается сброс флага с семантикой освобождения, а при вызове test_and_set()
(2)подразумевается стандартное упорядочение для операции установки флага и получения прежнего значения.
Объект std::atomic_flag
нельзя сконструировать копированием из другого объекта, не разрешается также присваивать один std::atomic_flag
другому. Это не особенность типа std::atomic_flag
, а свойство, общее для всех атомарных типов. Любые операции над атомарным типом должны быть атомарными, а для присваивания и конструирования копированием нужны два объекта. Никакая операция над двумя разными объектами не может быть атомарной. В случае копирования и присваивания необходимо сначала прочитать значение первого объекта, а потом записать его во второй. Это две отдельные операции над двумя различными объектами, и их комбинация не может быть атомарной. Поэтому такие операции запрещены.
Такая ограниченность функциональности делает тип std::atomic_flag
идеальным средством для реализации мьютексов-спинлоков. Первоначально флаг сброшен и мьютекс свободен. Чтобы захватить мьютекс, нужно в цикле вызывать функцию test_and_set()
, пока она не вернет прежнее значение false
, означающее, что теперь в этом потоке установлено значение флага true
. Для освобождения мьютекса нужно просто сбросить флаг. Реализация приведена в листинге ниже.
Листинг 5.1.Реализация мьютекса-спинлока с использованием std::atomic_flag
class spinlock_mutex {
std::atomic_flag flag;
public:
spinlock_mutex():
flag(ATOMIC_FLAG_INIT) {}
void lock() {
while (flag.test_and_set(std::memory_order_acquire));
}
void unlock() {
flag.clear(std::memory_order_release);
}
};
Это очень примитивный мьютекс, но даже его достаточно для использования в сочетании с шаблоном std::lock_guard<>
(см. главу 3). По своей природе, он активно ожидает в функции-члене lock()
, поэтому не стоит использовать его, если предполагается хоть какая-то конкуренция, однако задачу взаимного исключения он решает. Когда дело дойдет до семантики упорядочения доступа к памяти, мы увидим, как гарантируется принудительное упорядочение, необходимое для захвата мьютекса. Пример будет приведён в разделе 5.3.6.
Читать дальше