Такая ограниченность функциональности делает тип 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, который я рассмотрю ниже.

<p>5.2.3. Операции над <code>std::atomic<bool></bool></code></p>

Из атомарных целочисленных типов простейшим является std::atomic. Как и следовало ожидать, его функциональность в качестве булевского флага богаче, чем у std::atomic_flag. Хотя копирующий конструктор и оператор присваивания по-прежнему не определены, но можно сконструировать объект из неатомарного bool, поэтому в начальном состоянии он может быть равен как true, так и false. Разрешено также присваивать объектам типа std::atomic значения неатомарного типа bool:

std::atomic b(true);

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 b;

bool x = b.load(std::memory_order_acquire);

b.store(true);

x = b.exchange(false, std::memory_order_acq_rel);

Функция exchange() — не единственная операция чтения-модификации-записи, которую поддерживает тип std::atomic; в нем также определена операция сохранения нового значения, если текущее совпадает с ожидаемым.

Сохранение (или несохранение) нового значения в зависимости от текущего
Перейти на страницу:

Похожие книги