С. Мейерс - Эффективный и современный C++ (1114942), страница 59
Текст из файла (страница 59)
Но непонятно не только поведение заполнителей.Предположим, у нас есть функция для создания сжатых копий Widgetenum class CompLevel { Low, Norma l , High } ; / / Уровень сжатияWidget compress ( const Widget& w,CompLevel levl ;11 Создание сжатой11 копии wи мы хотим создать функциональный объект, который позволяет нам указывать, насколько сильно должен быть сжат конкретный объект Widget w. Представленное нижеприменение std : : Ьind создает такой объект:Widget w;using namespace std : : placeholders ;auto compressRateB=std : : Ыnd ( compre s s , w,1) ;Теперь, когда мы передаем w в std : : Ьi nd, он должен храниться для последующего вызова compress. Он сохраняется в объекте cornpressRateB, но как именно - по значениюили по ссылке! Это важно, потому что, если w изменяется между вызовами std : : Ьi ndи cornpres s Ra t eB, хранение w по ссылке будет отражать это изменение, в то время каксохранение по значению - нет.Ответ заключается в том, что оно хранится по значению1, но единственный способузнать это - просто запомнить: в вызове std : : Ь i nd нет никаких признаков этого.
Сравните этот подход с лямбда-выражением, в котором захват w по значению или по ссылкевыполняется явно:11 Захват w по значению;auto compressRateL/ / l ev передается по значению[w] (CornpLevel lev){ return cornpres s (w, lev) ; } ;Не менее явно в лямбда-выражения передаются и параметры. Здесь очевидно, что параметр lev передается по значению.
Таким образом,compressRateL (CompLevel : : High) ; // arg передается по значению1s t d : : b ind всегда копирует свои аргументы. но вызывающий код может добиться -эфф е ктасохранения аргумента по ссылке путем применения s t d : :auto cornpres sRateB=ref.Результат вызоваs t d : : Ыnd ( compress , s t d : : re f ( w ) , _1 ) ;состоит в том. что cornp r e s sRa t e B действует так, как если бы сохра11ялас1.
ссылкаа не его копия.242Глава 6. Лямбда-выраженияriaобъектw.Но как аргумент передается в объект, получающийся с помощью std : : Ь i nd?compressRateB ( CompLevel : : High ) ; 11 Как передается arg?И вновь единственный способ знать, как работает std : : Ьi nd, - это запомнить. (Ответ заключается в том, что все аргументы, передаваемые Ьind-объектам, передаютсяпо ссылке, поскольку оператор вызова функции для таких объектов использует прямуюпередачу.)По сравнению с лямбда-выражениями код, использующий std : : Ь ind, менее удобочитаем, менее выразителен и, возможно, менее эффективен. В С++ 14 нет обоснованныхслучаев применения std : : Ь i nd.
Однако в С++ 1 1 применение s t d : : Ь i nd может бытьоправдано в двух ограниченных ситуациях.•Лямбда-выражения С++ 1 1 не предоставляют возможностизахвата перемещением, но его можно эмулировать с помощью комбинации лямбдавыражения и std : : Ьi nd. Детали описываются в разделе 6.2, в котором также поясняется, что в С++ 14 поддержка инициализирующего захвата в лямбда-выраженияхустраняет необходимость в такой эмуляции.•Полиморфные функциональные объекты. Поскольку оператор вызова функцииЬind-объекта использует прямую передачу, он может принимать аргументы любоготипа (с учетом ограничений на прямую передачу, описанных в разделе 5.8).
Это может быть полезным, когда вы хотите связать объект с шаблонным оператором вызова функции. Например, для классаЗахват перемещением.class PolyWidget {puЬl i c :template<typename Т>void operator () (const Т& param) const;);s t d : : Ьind может связать Po lyWidget следующим образом:PolyWidget pw;auto boundPW=std : : bind ( pw,1) ;После этого boundPW может быть вызван с разными типами аргументов:boundPW ( l 930 ) ;/ / Передача int в PolyWidget : : operator ( )/ / Передача nullptr в PolyWidget : : operator ( )boundPW (nullptr ) ;boundPW ( "Rosebud" ) ; / / Передача строкового литералаВыполнить это с помощью лямбда-выражений С++ 1 1 невозможно. Однако в С++ 1 4этого легко достичь с помощью лямбда-выражения с параметром auto:6.
4 . Предпочитайте л ямбда-выражения п рименению std::Ьiпd243auto boundPW[pw] ( constauto¶m)11 С++ 1 4{ pw (param) ; } ;Конечно, это крайние случаи, и они встречаются все реже, поскольку поддержкаС++ 1 4 лямбда-выражений компиляторами становится все более распространенной.Когда Ь i nd был неофициально добавлен в С++ в 2005 году, это было существеннымусовершенствованием по сравнению с его предшественником 1 998 года.
Однако добавление поддержки лямбда-выражений в C++l l привело к устареванию s t d : : Ы nd, а с момента появления С++ 1 4 для него, по сути, не осталось применений.Сnедует зап омнить•Лямбда-выражения более удобочитаемы, более выразительны и могут быть болееэффеl\Гивными по сравнению с std : : Ы nd.•Только в С++ \ \ s t d : : Ы nd может пригодиться для реализации перемещающегозахвата или для связывания объеl\Гов с шаблонными операторами вызова функции.244Глава 6. Лямбда-выраженияГЛАВА 7Пара лл е ль н ы е вы чис л ен и яОдним из наибольших триумфов С++ 1 1 является включение параллелизма в языкпрограммирования и библиотеку.
Программисты, знакомые с другими потоковыми API(например, pthreads или Windows threads), иногда удивляются сравнительно спартанскому набору возможностей, предлагаемому С++, но это связано с тем, что по большей частиподдержка параллельности в С++ имеет вид ограничений для разработчиков компиляторов.
Получаемые в результате языковые гарантии означают, что впервые в истории С++программисты могут писать мноrопоточные приложения со стандартным поведениемна всех платформах. Это создает прочный фундамент, на котором могут быть построенывыразительные библиотеки, а элементы параллелизма в стандартной библиотеке (задачи,фьючерсы, потоки, мьютексы, переменные условий, атомарные объекты и прочие) являются лишь началом того, что обязательно станет более богатым набором инструментовдля разработки параллельного программного обеспечения на С++.В последующих разделах нужно иметь в виду, что стандартная библиотека содержитдва шаблона для фьючерсов: std : : future и std : : shared_ future.
Во многих случаях различия между ними не важны, так что я часто говорю просто о фьючерсах, подразумеваяпри этом обе разновидности.7 1 П редпочитайте про r раммирование на основезадач проrраммированию на основе потоков..Если вы хотите выполнить функцию doAs yncWork асинхронно, у вас есть два основных варианта. Вы можете создать std : : thread и запустить с его помощью doAsyncWork,тем самым прибегнув к подходу на основе потоков:int doAsyncWork ( ) ;std: : thread t ( doAsyncWork) ;Вы также можете передать doAsyncWor k в std : : async, воспользовавшись стратегией, известной как подход на основе задач:auto fut=std: : async (doAsyncWork) ; // " fut"от" future"В таких вызовах функциональный объект, переданный в s t d : : a s ync (например,doAsyncWork ) , рассматривается как задача (task).Подход на основе задач обычно превосходит свой аналог на основе потоков, и небольшиефрагменты кода, с которыми вы встретились выше, показывают некоторые причины этого.Здесь doAsyncwork дает возвращаемое значение, в котором, как мы можем разумно предположить, заинтересован вызывающий doAsyncwork код.
В случае вызова на основе потоковнет простого способа к нему обратиться. При подходе на основе потоков нет простого способа получить доступ к вызову. В случае же подхода на основе задач это можно легко сделать,поскольку фьючерс, возвращаемый std : : async, предлагает функцию get. Эта функция getеще более важна, если doAsyncwork генерирует исключение, поскольку get обеспечивает доступ и к нему. При подходе на основе потоков в случае генерации функцией doAsyncWorkисключения программа аварийно завершается (с помощью вызова s t d : : terminate ).Более фундаментальным различием между подходами на основе потоков и на основе задач является воплощение более высокого уровня абстракции в последнем. Он освобождаетвас от деталей управления потоками, что, кстати, напомнило мне о необходимости рассказать о трех значениях слова поток (thread) в параллельном программировании на С++.•Аппаратные потоки являются потоками, которые выполняют фактические вычисления.
Современные машинные архитектуры предлагают по одному или по нескольку аппаратных потоков для каждого ядра процессора.•Программные потоки (известные также как потоки ОС или системные потоки) являются потоками, управляемыми операционной системой1 во всех процессах и планируемыми для выполнения аппаратными потоками. Обычно можно создать программных потоков больше, чем аппаратных, поскольку, когда программный потокзаблокирован (например, при вводе-выводе или ожидании мьютекса или переменной условия), пропускная способность может быть повышена путем выполнениядругих, незаблокированных потоков.•s t d : : thread представляют собой объекты в процессе С++, которые действуют какдескрипторы для лежащих в их основе программных потоков.
Некоторые объектыstd : : t hread представляют "нулевые" дескрипторы, т.е. не соответствуют программным потокам, поскольку находятся в состоянии, сконструированном по умолчанию(следовательно, без выполняемой функции); потоки, из которых выполнено перемещение (после перемещения объект std : : thread, в который оно произошло, действует как дескриптор для соответствующего программного потока); потоки, у которых выполняемая ими функция завершена; а также потоки, у которых разорванасвязь между ними и обслуживающими их программными потоками.Программные потоки являются ограниченным ресурсом. Если вы попытаетесь создать их больше, чем может предоставить система, будет сгенерировано исключениеstd : : system error. Это так, даже если функция, которую вы хотите запустить, не генерирует исключений.
Например, даже если doAsyncwork объявлена как noexcept,_int doAsyncWork ( ) noexoept; / / См . noexcept в разделе 3 . 81 В предположении, что таковая имеется. В некоторых встроенных системах ее нет.246Глава 7. Параллельные вычисленияследующая инструкция может сгенерировать исключение:std : : thread t ( doAsyncWork) ; / / Генерация исключения , если// больше нет доступных потоковХорошо написанное программное обеспечение должно каким-то образом обрабатывать такую возможность, но как? Один вариант - запустить doAsyncWork в текущемпотоке, но это может привести к несбалансированной нагрузке и, если текущий потокявляется потоком GUI, к повышенному времени реакции системы на действия оператора. Другой вариант - ожидание завершения некоторых существующих программныхпотоков с последующей попыткой создания нового объекта std : : thread, но может бытьи так, что существующие потоки ожидают действий, которые должна выполнить функция doAsyncWork (например, ее результата или уведомления переменной условия).Даже если вы не исчерпали потоки, у вас могут быть проблемы с превышением подписки (oversubscription).
Это происходит, когда имеется больше готовых к запуску (т.е.незаблокированных) программных потоков, чем аппаратных. Когда это случается, планировщик потоков (который обычно представляет собой часть операционной системы) выполняет разделение времени для выполнения программных потоков аппаратными. Когдавремя работы одного потока завершается и начинается время работы второго, выполняется переключение контекстов. Такие переключения контекстов увеличивают накладныерасходы по управлению потоками и могут оказаться в особенности дорогостоящими,когда аппаратный поток, назначаемый программному, оказывается выполняемым другимядром, не тем, что ранее. В этом случае ( 1 ) кеши процессора обычно оказываются с данными, не имеющими отношения к данному программному потоку, и (2) запуск "нового"программного потока на этом ядре "загрязняет" кеши процессора, заполняя их данными,не имеющими отношения к "старым" потокам, которые выполнялись этим ядром и, вероятно, будут выполняться им снова.Избежать превышения подписки сложно, поскольку оптимальное отношение программных и аппаратных потоков зависит от того, как часто запускаются программныепотоки, и может изменяться динамически, например, при переходе программы от работы по вводу-выводу к вычислениям.