С. Мейерс - Эффективный и современный C++ (1114942), страница 26
Текст из файла (страница 26)
(Простое действие, заключающееся в добавлении в функцию отладочного вывода для отладкиили настройки производительности может привести к таким проблемам, поскольку инструкции ввода-вывода в общем случае в соnst ехрr-функциях недопустимы.) Часть "гдеэто возможно" совета является вашей доброй волей на придание долгосрочного характера данному ограничению на объекты и функции, к которым вы его применяете.Сл едует запомнить•Объекты const expr являются константными и инициализируются объектами, значения которых известны во время компиляции.•Функции const expr могут производить результаты времени компиляции при вызове с аргументами, значения которых известны во время компиляции.•Объекты и функции constexpr могут использоваться в более широком диапазонеконтекстов по сравнению с объектами и функциями, не являющимися constexpr.•constexpr является частью интерфейса объектов и функций.3 .
1 0. Делайте константные функ ц ии-членыбезопасными в смысле потоковЕсли мы работаем в области математики, нам может пригодиться класс, представляющий полиномы. В этом классе было бы неплохо иметь функцию для вычисления корнейполинома, т.е. значений, при которых значение полинома равно нулю. Такая функция недолжна модифицировать полином, так что ее естественно объявить как const:class Polynomial {puЫic :11 Структура данных , хранящаяusing RootsTypestd : : vector<douЫe>; / / значения, где полином равен нулю3 . 1 О.Д елайте константные функции-члены безопасными в смысле потоков11111 ( см . "using" в разделе3 .
3)Roots'l'ype roots О conзt;};Вычисление корней - трудная дорогостоящая операция, так что мы не хотим их вычислять до того, как они реально потребуются. Но если они нам требуются, то, определенно, требуются не один раз. Поэтому мы будем кешировать корни полиномов, еслинам приходится их вычислять, и реализуем roots так, чтобы функция возвращала кешированное значение.
Вот как выглядит такой подход:class Polynomial {puЫic :using RootsTypestd: : vector<douЫe> ;RootsType roots ( ) constif ( ! rootsAreVa lid)rootsAreVa l id11 Если кеш некорректен,1 1 вычисляем корни и сохраняем11 их в in rootVa l st rue;return rootVa l s ;private :mutaЫe bool rootsAreValid{ fal s e } ; // См . инициализаторы11 в разделе 3 . 1mutaЫe RootsType rootVal s { } ;};Концептуально roots не изменяет объект Polynomial, с которым работает, но в качествечасти кеширующих действий может потребоваться изменение rootVal s и rootsAreVal id.Это классический случай использования mutaЫe, и именно поэтому эти данные-членыобъявлены с данным модификатором.Представим теперь, что два потока одновремен но вызывают roots для объектаPol ynomial:Polynomial р ;*// * ----- Поток 1 -auto rootsOfP : p .
roots ( ) ;---/ * ------- Поток 2 ---*/auto valsGivingZero : p . roots ( ) ;---Этот клиентский код совершенно разумен. Функция root s является константнойфункцией-членом, и это означает, что она представляет операцию чтения. Выполнениеопераций чтения несколькими потоками одновременно без синхронизации вполне безопасно. Как минимум предполагается, что это так. В данном случае это не так, посколькув функции root s один или оба эти потока могут попытаться изменить члены-данныеroot sAreVal id и rootVa l s. Это означает, что данный код может одновременно читать1 12Гnава 3. Переход к современному С++и записывать одни и те же ячейки памяти без синхронизации, а это - определение гонкиданных. Такой код имеет неопределенное поведение.Проблема заключается в том, что функция roots объявлена как const, но не является безопасной с точки зрения потоков. Объявление const является корректным какв С++ l l , так и в С++98 (вычисление корней полинома не изменяет сам полином), так чтокоррекция нужна для повышения безопасности потоков.Простейший способ решения проблемы обычно один: применение rnutex:class Polynornia l {puЫ i c :std : : vector<douЫe > ;using RootsTypeRootsType roots ( ) const{std: : lock_guard<std : : mutex> g (m) ; 11 Блокировка мьютексаif ( ! root sAreValid) {1111Если кеш некорректенВычисление корней11Разблокированиеroot sAreValid = t rue ;return rootVals;private :IПUtaЫe std: : mutex m;mutaЬle bool rootsAreVa lid{ false } ;rnutaЬle RootsType rootVals { } ;};Мьютекс s t d : : mutex rn объявлен как rnutaЬle, поскольку его блокировка и разблокирование являются неконстантными функциями, а в противном случае в константнойфункции-члене roots мьютекс rn рассматривается как константный объект.Следует отметить, что поскольку s t d : : rnutex не может быть ни скопирован, ни перемещен, побочным эффектом добавления rn к Polynornia l является то, что Polynornial теряет возможность копирования и перемещения.В некоторых ситуациях мьютекс является излишеством.
Например, если все, что выделаете, - это подсчитываете, сколько раз вызывается функция-член, то часто более дешевым средством является счетчик s t d : : atornic (т.е. счетчик, для которого гарантируется атомарность операций - см. раздел 7.6). (Действительно ли это более дешевое средство, зависит от аппаратного обеспечения и реализации мьютексов в вашей стандартнойбиблиотеке.) Вот как можно использовать std: : atornic для подсчета вызовов:11class PointpuЬli c :douЬle distanceFrornOrig in ( )const noexcept++callCount;3 .
1 О.Двумерная точка/ / См . описание noexcept// в разделе 3 . 8/ / Атомарный инкрементДелайте константные функции-члены безопасными в смысле потоков113return std : : hypot ( x , у ) ; // std: : hypot-новинкаC++ l lprivate :mutaЬle std : : atomic<ШlSigned> callCoшit{ О } ;douЫeх,у;1;Как и s t d : : rnutex, s t d : : at ornic невозможно копировать и неперемещать, так что наличие cal lCount в Point означает, что Point также невозможно и перемещать.Поскольку операции над переменными s t d : : a t ornic зачастую менее дорогостоящи,чем захват и освобождение мьютекса, вы можете соблазниться использовать s t d : : atornicбольше, чем следует.
Например, в классе, кеширующем дорогостоящее для вычислениязначение i n t , вы можете попытаться использовать вместо мьютекса пару переменныхstd : : atornic:class WidgetpuЬlic :int magicValue ( ) constif ( cacheValid) return cachedValue;else {auto val lexpensiveComputationl ( ) ;auto val2expensiveComputat ion2 ( ) ;cachedValue = vall + val2 ; / / Часть 1/ / Часть 2cachevalidtrue ;return cachedValue;===private :mutaЫe std: : atomic<Ъool> cacheValid{ false ) ;mutaЫe std: : atomic<int> cachedValue ;1;Этот способ работает, но иногда выполняет существенно большую работу, чем требуется. Рассмотрим такой сценарий.•Поток вызывает W i dget : : rna g i cValue, видит, что ca cheVa l i d равно f a l s e , выполняет два дорогостоящих вычисления и присваивает их сумму переменнойca chedVal ue.•В этот момент второй поток вызывает W i dge t : : ma g i cVa lue, также видит, чтозначение cacheVal id равно f a l se, а потому выполняет те же дорогостоящие вычисления, что и только что завершивший их первый поток.
(Этот "второй поток"на самом деле может быть несколькими другими потоками.)Чтобы справиться с этой проблемой, можно пересмотреть порядок присваиваний значений переменным cachedValue и cacheVa l i d, но вы вскоре поймете, что ( 1 ) вычислять114Гn а ва 3 . Переход к современному С++v a l l и v a l 2 перед тем, как cacheVa l i d устанавливается равным t rue, по-прежнему могут несколько потоков, тем самым провалив цель нашего упражнения, и (2) на самомделе все может быть еще хуже:class WidgetpuЬli c :int magicValue ( ) constif ( cacheVa l id) return cachedValue ;else {auto val lexpens iveComputat ionl ( ) ;auto val2expensiveComputation2 ( ) ;/ / Часть 1cacheValid = true ;return cachedValuevall + val2 ; / / Часть 2===};Представим, что значение cacheVal id равно false.
Тогда возможно следующее.•Один поток вызывает W i dget : : magi cValue и выполняет код до точки, где переменная cacheVal i d устанавливается равной true.•В этот момент второй поток вызывает Widget : : magicValue и проверяет значениеcacheVal id.
Увидев, что оно равно t rue, поток возвращает cachedValue, несмотряна то, что первый поток еще не выполнил присваивание этой переменной. Такомобразом, возвращенное значение оказывается некорректным.Это неплохой урок. Для единственной переменной или ячейки памяти, требующей синхронизации, применение std : : atomic является адекватным решением, но как толькоу вас имеется две и более переменных или ячеек памяти, которыми надо оперироватькак единым целым, вы должны использовать мьютекс. Для Widget : : magicValue это выглядит следующим образом:class WidgetpuЫic :int magicValue ( ) conststd : : lock_guard<std : : mutex>guard (m) ; / / Блокировка mi f ( cacheVal id) return cachedValue ;else {auto val lexpensiveComputa tionl ( ) ;auto val2expensiveComputation2 ( ) ;cachedValueval l + val 2 ;cacheVal id = true;===3.1 О.Де л айте константные функции-члены безопасными в смысле потоков115return cachedValue;private :mutaЫe std : : mutex m;mutaЫe int cachedValue ;mutaЫe bool cacheValid{ false ) ;);11Разблокирование m11Не атомарноеНе атомарное11Сейчас данный раздел основывается на предположении, что несколько потоков могут одновременно выполнять константную функцию-член объекта.
Если вы пишете константную функцию-член там, где это не так - т.е. там, где вы можете гарантировать, что этафункция-член объекта никогда не будет выполняться более чем одним потоком, - безопасность с точки зрения потоков является несущественной. Например, совершенно неважно, являются ли безопасными с точки зрения потоков функции-члены классов, разработанные исключительно для однопоточного применения. В таких случаях вы можетеизбежать расходов, связанных с мьютексами и std : : atomic, а также побочного эффекта,заключающегося в том, что содержащие их классы становятся некопируемыми и неперемещаемыми.
Однако такие сценарии, в которых нет потоков, становятся все более редкими и, вероятно, дальше будут становиться только все более редкими. Безопаснее считать, что константные функции-члены будут участвовать в параллельных вычислениях,и именно поэтому следует гарантировать безопасность таких функций с точки зренияпотоков.Сл едует запомн ить•Делайте константные функции-члены безопасными с точки зрения потоков,если только вы не можете быть уверены, что они гарантированно не будут использоваться в контексте параллельных вычислений.•Использование переменных std : : a t om i c может обеспечить более высокуюпо сравнен ию с мьютексами производительность, но они годятся толькодля работы с единственной переменной или ячейкой памяти.3 .1 1 .