5.2.6. Основной шаблон класса std::atomic<>
Наличие основного шаблона позволяет создавать атомарные варианты пользовательских типов, в дополнение к стандартным атомарным типам. Однако в качестве параметра шаблона std::atomic<> может выступать только тип, удовлетворяющий определенным условиям. Точнее, чтобы тип UDT мог использоваться в конструкции std::atomic<UDT>, в нем должен присутствовать тривиальный оператор присваивания. Это означает, что в типе не должно быть виртуальных функций или виртуальных базовых классов, а оператор присваивания должен генерироваться компилятором. Более того, в каждом базовом классе и нестатическом члене данных также должен быть тривиальный оператор присваивания. Это позволяет компилятору использовать для присваивания функцию memcpy() или эквивалентную ей, поскольку исполнять написанный пользователем код не требуется.
Наконец, тип должен допускать побитовое сравнение на равенство. Это требование из того же разряда, что требования к присваиванию — должна быть не только возможность колировать объекты с помощью memcpy(), но и сравнивать их с помощью memcmp(). Это необходимо для правильной работы операции сравнить-и-обменять.
Чтобы понять, чем вызваны такие ограничения, вспомните рекомендацию из главы 3: не передавать ссылки и указатели на защищенные данные за пределы области видимости в виде аргументов предоставленной пользователем функции. В общем случае компилятор не в состоянии сгенерировать свободный от блокировок код для типа std::atomic<UDT>, поэтому он вынужден применять внутренние блокировки. Если бы пользовательские операторы присваивания и сравнения были разрешены, то пришлось бы передавать ссылку на защищенные данные в пользовательскую функцию, нарушая тем самым приведённую выше рекомендацию. Кроме того, библиотека вправе использовать единую блокировку для всех нуждающихся в ней атомарных операций, поэтому, разрешив вызывать пользовательские функции в момент, когда эта блокировка удерживается, мы могли бы получить взаимоблокировку или надолго задержать другие потоки, если сравнение занимает много времени. Наконец, эти ограничения повышают шансы на то, что компилятор сумеет сгенерировать для std::atomic<UDT> код, содержащий истинно атомарные команды (и тем самым обойтись в данной конкретизации вообще без блокировок), поскольку в этой ситуации он вправе рассматривать определенный пользователем тип как неструктурированную последовательность байтов.
Отметим, что несмотря на то, что типы std::atomic<float> и std::atomic<double> формально разрешены, так как встроенные типы с плавающей точкой удовлетворяют сформулированным выше критериям на использование memcpy и memcmp, их поведение в части функции compare_exchange_strong может оказаться неожиданным. Операция может завершиться отказом, даже если ранее сохраненное значение численно равно ожидаемому, но имеет другое внутреннее представление. Отметим также, что над числами с плавающей точкой не определены атомарные арифметические операции. Аналогичное поведение compare_exchange_strong вы получите, если конкретизируете std::atomic<> пользовательским типом, в котором оператор сравнения на равенство определён, но отличается от сравнения с помощью memcmp — операция может завершиться отказом, потому что равные значения имеют различное представление.
Если размер пользовательского типа UDT равен (или меньше) размеру int или void*, то на большинстве платформ для типа std::atomic<UDT> можно сгенерировать код, содержащий только атомарные команды. На некоторых платформах подобный код можно сгенерировать и в случае, когда размер пользовательского типа в два раза превышает размер int или void*. Обычно это платформы, на которых имеется команда сравнения и обмена двойных слов double-word-compare-and-swap (DWCAS), соответствующая функциям compare_exchange_xxx.
В главе 7 мы увидим, что такая поддержка может быть полезна для написания кода без блокировок. В силу описанных ограничений вы не можете создать, к примеру, тип std::atomic<std::vector<int>>, но можете использовать для параметризации классы, содержащие счетчики, флаги, указатели и даже массивы простых элементов. Обычно это не проблема; чем сложнее структура данных, тем больше вероятность, что в ней нужно будет определить какие-то другие операции, помимо простейшего присваивания и сравнения. Но в таком случае лучше воспользоваться классом std::mutex, который гарантирует надлежащую защиту данных при выполнении этих операций (см. главу 3).
Интерфейс шаблона std::atomic<T>, конкретизированного пользовательским типом T, ограничен набором операций, доступных классу std::atomic<bool>: load(), store(), exchange(), compare_exchange_weak(), compare_exchange_strong(), присваивание значения типа T и преобразование в значение типа T.
В табл. 5.3 перечислены операции, доступные для всех атомарных типов.
Таблица 5.3. Операции над атомарными типами
Операция atomic_ flag atomic <bool> atomic <T*> atomic <integral- type> atomic <other-type> test_and_set √ clear √ is_lock_free √ √ √ √ load √ √ √ √ store √ √ √ √ exchange √ √ √ √ compare_exchange_weak, compare_exchange_strong √ √ √ √ fetch_add, += √ √ fetch_sub, -= √ √ fetch_or, |= √ fetch_and, &= √ fetch_xor, ^= √ ++, -- √ √