Такая ограниченность функциональности делает тип 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.
Тип std::atomic_flag настолько ограничен, что его даже нельзя использовать в качестве обычного булевского флага, так как он не допускает проверки без изменения значения. На эту роль больше подходит тип std::atomic, который я рассмотрю ниже.
5.2.3. Операции над std::atomic
Из атомарных целочисленных типов простейшим является std::atomic. Как и следовало ожидать, его функциональность в качестве булевского флага богаче, чем у std::atomic_flag. Хотя копирующий конструктор и оператор присваивания по-прежнему не определены, но можно сконструировать объект из неатомарного bool, поэтому в начальном состоянии он может быть равен как true, так и false. Разрешено также присваивать объектам типа std::atomic значения неатомарного типа bool:
std::atomic
b = false;
Что касается оператора присваивания с неатомарным bool в правой части, нужно еще отметить отход от общепринятого соглашения о возврате ссылки на объект в левой части — этот оператор возвращает присвоенное значение типа bool. Такая практика обычна для атомарных типов: все поддерживаемые ими операторы присваивания возвращают значения (соответствующего неатомарного типа), а не ссылки. Если бы возвращалась ссылка на атомарную переменную, то программа, которой нужен результат присваивания, должна была бы явно загрузить значение, открывая возможность для модификации результата другим потоком в промежутке между присваиванием и чтением. Получая же результат присваивания в виде неатомарного значения, мы обходимся без дополнительной операции загрузки и можем быть уверены, что получено именно то значение, которое было сохранено.
Запись (любого значения: true или false) производится не чрезмерно ограничительной функцией clear() из класса std::atomic_flag, а путём вызова функции-члена store(), хотя семантику упорядочения доступа к памяти по-прежнему можно задать. Аналогично вместо test_and_set() используется более общая функция-член exchange(), которая позволяет атомарно заменить ранее сохраненное значение новым и вернуть прежнее значение. Тип std::atomic поддерживает также проверку значения без модификации посредством неявного преобразования к типу bool или явного обращения к функции load(). Как нетрудно догадаться, store() — это операция сохранения, load() — операция загрузки, a exchange() — операция чтения-модификации-записи:
std::atomic
bool x = b.load(std::memory_order_acquire);
b.store(true);
x = b.exchange(false, std::memory_order_acq_rel);
Функция exchange() — не единственная операция чтения-модификации-записи, которую поддерживает тип std::atomic; в нем также определена операция сохранения нового значения, если текущее совпадает с ожидаемым.