С. Мейерс - Эффективный и современный C++ (1114942), страница 63
Текст из файла (страница 63)
Это необходимо, поскольку применение j o i n или detach к неподключаемому объекту приводитк неопределенному поведению. Может быть так, что клиент создает std : : th read,затем создает из неrо объект ThreadRAI I , использует функцию-член get для получения доступа к t, а затем выполняет перемещение из t или вызывает для него j o inили detach. Каждое из этих действий делает t неподключаемым.if ( t . joinaЬle ( ) ) {if (action == DtorAction : : join)t . j oin ( ) ;else {t . deta.ch о ;Если в приведенном фрагменте вас беспокоит возможность условия гонки из-за того,что между вызовами t .
j oi naЫe { ) и j oi n или detach другой поток может сделатьt неподключаемым, то ваша интуиция заслуживает похвалы, но ваши опасенияв данном случае беспочвенны. Объект std : : thread может изменить состояниес подключаемого на неподключаемое только путем вызова функции-члена, напримерj oin, detach или операции перемещения. В момент вызова деструктора ThreadRAI Iникакие другие потоки не должны вызывать функцию-член для этого объекта.
При2 58Гnа ва 7 . Параnnеnьные вычисnениянаnичии одновременных вызовов, определенно, имеется условие rонки, но не внутридеструктора, а в клиентском коде, который пытается вызвать одновременно двефункции-члена объекта (деструктор и что-то еще). В общем случае одновременныевызовы функций-членов для одного объекта безопасны, только если все ониявляются константными функциями-членами (см. раздел 3.
1 0).Использование ThreadRAI I в нашей функции doWor k может выглядеть следующимобразом:bool doWor k ( s td : : function<bool ( int ) > filter, // Как и ранееint maxVal = tenМi l l ion)std : : vector<int> goodVa l s ;11 Как и ранееТhreadRAII t ( // u s e RAI I obj ectstd : : thread ( [ & fi lter, maxVal , &goodVa l s ]{for ( auto i = О ; i <= maxVal ; ++ i ){ i f ( filter ( i ) ) goodVal s .
push_back ( i ) ;}) 'ТhreadRAII : : DtorAction : : join1 1 Действие RAI I);auto nh = t . get ( ) . native_handle ( ) ;if( condi t i onsAreSa t is fi ed () )t . get () . j oin ( ){;performCompu ta t ion (goodVa l s ) ;return true ;return fa l s e ;В этом случае мы выбрали использование j o in для асинхронно выполняющегося потока в деструкторе ThreadRAI I, поскольку, как мы видели ранее, применение detach можетпривести к настоящим кошмарам при отладке.
Мы также видели ранее, что применениеj oin может вести к аномалиям производительности (что, откровенно говоря, также может быть неприятно при отладке), но выбор между неопределенным поведением (к которому ведет detach), завершением программы (при использовании обычного std : : thread)и аномалиями производительности предопределен - мы выбираем меньшее из зол.Увы, раздел 7.5 демонстрирует, что применение ThreadRAI I для выполнения j o i n приуничтожении std : : t hread иногда может привести не к аномалии производительности, а кполному "зависанию" программы.
"Правильным" решением этого типа проблем было бы сообщить асинхронно выполняющемуся лямбда-выражению, что в ero услугах мы больше ненуждаемся и оно должно поскорее завершиться. Увы, С++ 1 1 не поддерживает прерь1ваемыепотоки. Их можно реализовать вручную, но данный вопрос выходит за рамки нашей книги4.4Вы можете обратиться к книге Энтони Вильямса (Anthony Wil\iams) С++ Concurrency in Action(Manning Puhlications, 20 1 2), раздел 9.2.7.3.Деnайте std::thread неподкnючаемым на всех путях выпоnнения259В разделе 3. 1 1 поясняется, что, поскольку T h r eadRAI I объявляет деструктор, в немнет генерируемых компилятором перемещающих операций, но причин, по которымThreadRA I I не должен быть перемещаемым, тоже нет.
Если бы компилятор генерировалтакие функции, то они демонстрировали бы верное поведение, так что просто попросимкомпилятор их все же сгенерировать:class ThreadRAI I {puЫ ic :enum class DtorActionj oin, detach ) ;ThreadRAI I ( st d : : thread&& t , DtorAct ion а ): action ( а ) , t ( st d : : move ( t ) ) { )11 Как и ранее11 Как и ранее-ThreadRAI I ( ){11 Как и ранее/ / ПоддержкаТhreadRAII (ТhreadRAII&&) =clefault;ТhreadRAII& operator= (ТhreadRAII&&) =clefault ; // перемещенияstd : : thread& get ( ) { return t ; )1 1 Как и ранее11 Как и ранееprivate :DtorActioп action;std : : thread t ;);Следует запомнить•Делайте s t d : : t hread неподключаемыми на всех путях выполнения.•Применение j oi n при уничтожении объекта может привести к трудно отлаживаемым аномалиям производительности.•Применение d e t a ch при уничтожении объекта может привести к трудно отлаживаемому неопределенному поведению.•Объявляйте объекты s t d : : t h re a d в списке членов-данных последними.7 .4.
П омните о разном поведениидеструкторов дескрип торов потоковВ разделе 7.3 вы узнали, что подключаемый s t d : : thread соответствует базовому системному потоку выполнения. Фьючерс для неотложенной задачи (см. раздел 7.2) имеетсхожую связь с системным потоком. А раз так, и объекты s t d : : t hread, и объекты фьючерсов можно рассматривать как дескрипторы (handles) системных потоков.260Глава 7 . Па раллельные вычисленияС этой точки зрения интересно, что std : : thread и фьючерсы совершенно по-разномуведут себя в деструкторах.
Как упоминалось в разделе 7.3, уничтожение подключаемогоs t d : : t hread завершает работу программы, поскольку две очевидные альтернативы неявный вызов j o i n и неявный вызов detachоказываются еще более плохим выбором. Однако деструктор фьючерса ведет себя так, как если бы и ногда выполнялся неявный вызов j oin, иногда - неявный вызов detach, а иногда - ни то и ни другое. Онникогда не приводит к завершению работы программы. Поведение этого дескрипторапотока заслуживает более внимательного рассмотрения.Начнем с наблюдения, что фьючерс представляет собой один из концов канала связи,по которому вызываемая функция передает результаты вызывающей5• Вызываемая функция(обычно работающая асинхронно) записывает результат вычислений в коммуникационныйканал (обычно с помощью объекта std : : promise), а вызывающая функция читает результатс помощью фьючерса.
Вы можете представлять это для себя следующим образом (пунктирные стрелки показывают поток информации от вызываемой функции к вызывающей):-std : : promis e Вызываемая 1Выз ывающая . фьючерсФункция- - - - --- - - - --- - - --- - - --- -!обi.�но) функцияНо где же хранится результат вызываемой функции? Вызываемая функция может завершиться до того, как будет вызвана функция-член get соответствующего фьючерса, такчто результат не может быть сохранен в std : : promi se вызываемой функции. Этот объект,будучи локальным по отношению к вызываемой функции, уничтожается по ее завершении.Результат не может храниться и во фьючерсе вызывающей функции, поскольку(среди прочих причин) s td : : future может быть использован для создания объектаs t d : : sha red future (тем самым передавая владение результатом вызываемой функцииот s t d : : future в std : : sha red_future), который затем, после уничтожения исходногоs t d : : fut ure, может быть многократно копирован.
С учетом того, что не все типы результата могут быть скопированы (например, существуют только перемещаемые типы)и что результат должен существовать до тех пор, пока как минимум последний фьючерсна него ссылается, какой из потенциально многих фьючерсов, соответствующих вызываемой функции, должен содержать ее результат?Поскольку ни объекты, связанные с вызываемой функцией, н и объекты, связанныес вызывающей функцией, не являются подходящими местами для хранения результатавызываемой функции, они должны храниться где-то вне этих объектов.
Такое местоположение известно как общее состояние (shared state). Это общее состояние обычно представлено объектом в динамической памяти, но его тип, интерфейс и реализация в стандарте языка не указаны. Авторы стандартной библиотеки могут реализовывать общиеСОСТОЯНИЯ так, как хотят.5 В разделе7.5 поясняется, что разновидность канапа связи фьючерса может быть использованаи для других целей. Однако в этом разделе мы будем рассматривать только его применение в качестве механизма дпя передачи резупьтата из вызываемой функции вызывающей.7.4. Помните о разном поведении деструкторов дескрипторов потоков261Мы можем представить себе отношения между вызываемой функцией, вызывающейфункцией и общим состоянием следующим образом (пунктирными стрелками вновьпредставлен поток информации):Общее состояниесВызывающая · Ф�'О.11!!!.. _ _ _ _ _ _ Результат , _ _s_to.:.