А. Александреску - Современное проектирование на C++ (1119444), страница 40
Текст из файла (страница 40)
Следует отметить, что реализация функции 5есьопцечтту довольно проста. Она создает конкретный объект класса ь(бестветгаскег, добавляет его в стек и регистрирует вызов функции асех1с. Приведенный ниже код очень важен для обобщения. Он вводит в рассмотрение функторы, предназначенные для уничтожения отслеживаемых объектов. Это позволяет не использовать оператор де1есе для удаления объекта из динамической памяти, поскольку может оказаться, что объект находится в альтернативной куче илн тле-то сше.
По умолчанию механизм уничтожения обьскта представляет собой указатель на функцию, вызывающую оператор де1есе. Эта функция называется Ое1есе, а ее шаблонный параметр задает тип удаляемого объекта. // вспомогательная функция для удаления объектов севр1асе <сурепаве т> эсгцсс Ое1есег эсастс чо1д ое1есе(т+ рОЬ)) ( де1ете РОЬ)! ) О конкретный обьект класса ст'Еестветгаскег для объектов типа т севр1асе <сурепаве т, сурепаве Оеэсгоуег> с1аьэ сопсгесестрес)ветгас!сег : рцЬ)тс ь!Еесзветгас!сег ' Фактически функция бесьопдечтсу использует толька функцию эсд::геа11ос, заменяющую собой н функцию ва11ос, н функцию тгее. Если вызвать се с нулевым уквзатслсч, она велст себя как функция эсд::ва11ос, в если завять нулевой рвзмср, онэ имитирует работу функции эсд::тгее.
165 Глава 6, Реализация шаблона 8!пц!етоп рцЬ11с: сопсгетеь1Еет1иетгас1ег(т* р, цпэ1доед 1пт 1оядеч1ту, реэтгоуег д) :ь1тет1иетгасйег(1опдеч1су), ртгаскед (р), деэтгоуег (д) () -сопсгетеь1тет1иетгас1егО деэтгоуег (ртгас)сед ); рг1чате: т* ртгасйед ; пеэтгоуег деэтгоуег; чо1д лтях1тяпО; // необходимое объявление теир1ате <турепаие т, турепаве оеэтгоуег> чо1д эетьопдеч1ту(т* рпупоЬ)ест, 1пэ1дпед 1пт 1опдеч1ту, пеэтгоуег д = яг1чате::пе1есег<т>::ое1ете) тгас)сеглггау рнеилггау = этатзс саэт<тгасйеглггау>( этд::геа11ос(ртгаскеглггау, е1еиептэ + 1)); 1т (1рнеилггау) тЬгои этд::Ьад а11ос0; ь11ет1иетгас~ег* р = пеи сопсгесеь1Фетзиетгас1ег <т, пеэтгоуег>(рпупоЬ)ест, 1опдеч1ту, д); ртгасКеглггау = рнеилггау; тгас)сеглггау роэ = этд::иррег Ьоцпд( ртгасйеглггау, ртгасКеглггау + е1еиептэ, 1опдечдсу, Соираге); этд::сору Ьас)сиагд(роэ, ртгас1еглггау + е1еиептэ, ртгас)сегаггау + е1еиепгэ + 1); *роз = р; ++е1еиептэ; этд::атехдт(лтвх)тяп); Использование функций этд::цррег Ьоипд и эгд::сору Ьас)сиагд намного облегчает чтение и понимание этого нетривиального кода.
Описанная выше функция вставляет вновь созданный указатель на обьект класса сопсгетеь1тет1иетгас)сег в упорядоченный массив, на который ссылается указатель ртгаскеглггау, сохраняет порядок следования ето элементов, а также обрабатывает ошибки и возникаюшие исключительные ситуации. Теперь цель функции ь1Фет1иетгас)сег::соираге проясняется. Массив, на который ссылается указатель ртгасйегциеие, упорядочен в соответствии с продолжительностью жизни объектов. Обьекты с наибольшей продолжительностью жизни находятся в начале массива.
Объекты с одинаковой продолжительностью жизни следуют в соответствии с очередностью их вставки. функция лтях1тгп выталкивает объект с наименьшей продолжительностью жизни (т,е. последний элемент массива) и удаляет его. Удаление указателя на объект класса ь11ет1иетгас)сег приводит к вызову деструктора класса сопсгетеь1тет1иетгас)сег, который в свою очередь удаляет объект. этатдс чо1д атях1тяпО аьвегт(е1еиепгэ > О $$ ртгасйеглггау != О); // выбираем элемент, находящийся на вершине стека ь1тет1иетгасМег* ртор = ртгасйеглггау[е1еиептэ — Ц; 166 Часть И.
Компоненты // удаляем этот объект из стека. // ошибки не проверяются -- функция геа11ос, // примененная к меньшему размеру памяти, // всегда работает правильно ртгаскегдггау = (тгаскегдггау*)згд::геа11ос(ртгаскегдггау, — е1еюепгз); // Уничтожаем элемент де1еге ртор; Созлавая функцию агах)геп, следует быть внимательным. Она должна выталки- вать из стека элемент, находяшийся на вершине, и удалять его. В свою очередь дест- руктор удаляемого элемента ликвидирует управляемый им объект. Проблема заключа- ется в том, что функция дгвх1гЕп должна вытолкнуть объект из вершины стека до его удаления, поскольку разрушение одного объекта может повлечь за собой создание другого, который будет затолкнут обратно в стек.
Несмотря на то что это выглядит до- вольно необычно, именно так развиваются события, когда деструктор класса кеу- Ьоагд пытается использовать объект класса год. По умолчанию код скрывает структуры данных, и функция дгдх)геп находится в пространстве имен ег)чаге. Пользователи могут любоваться лишь вершиной айсбер- га — функцией 5егсопдеч1гу. Синглтоны с заданной продолжительностью жизни могут использовать функцию 5еггопдеч1гу следуюшим образом. с1аьз сод риЬ1)с: згаг1с чо1д сгеагеО ( // создаем экземпляр ртпзгапсе = пеи сод; // эти строки добавлены 5егсопдеч\гу(*гЬ15, 1опдеч)гу ); ) // Остальная часть реализации пропущена. // еункция год::тпзгапсе определяется, как и прежде.
рг1чаге: // определяем фиксированную продолжительность жизни ьгаг)с сопьг мпз)диет )пг 1опдеч1гу = 2; эгаг)с сод* ртпзгапсе ; ); Если реализовать классы кеуЬоагб и о1зр1ау, следуя описанному выше подходу, но задать продолжительность жизни их объектов равной 1, то объект класса год их обязательно переживет. Решает ли это проблему К1)Е? Что если приложение использует несколько потоков? 6.9. Продолжительность жизни обьектов в многопоточной среде Синглтоны должны работать и с потоками тоже.
Допустим, наше приложение только что начало работу, и два потока имеют доступ к привеленному ниже синглтону. 51пд1егопй 5)пд1егоп::тпвгапсеО ( 1Ф(!ртпэгапсе) // 1 1Б7 Глава б. Реализация шаблона 31пц!егоп с р1ПЗГапсе = пев 5)пц1егоп; // 2 гесигп *р1пзтапсе ; // 3 ) Первый поток входит в функцию 1озтаосе и проверяет условие оператора 11. Поскольку поток входит в функцию впервые, значение переменной ртпзтапсе равно О. В таком случае поток управления достигает строки с комментарием //2 и готовится вызвать оператор пею, Планировшик заданий операционной системы может прервать первый поток в этой точке и передать управление другому потоку. Второй поток вызывает функцию 5)пц1етоп::1пзтапсе() и обнаруживает, что значение переменной р1пзтаосе также равно нулю, поскольку первый поток ее еше не менял, До сих пор первый поток только проверял значение переменной р1пзтапсе .
Теперь второй поток вызывает оператор пею, присваивает переменной р1пзтапсе некий адрес и покидает функцию. К несчастью. первый поток снова приходит в сознание, вспоминает, что осталось выполнить лишь строку с комментарием //2, изменяет значение переменной р1пзтапсе и выхолит из функции. Когда пыль рассеивается, оказывается, что вместо олного обьекта класса 5зпц)егоп созданы лва, причем один из них очевидно лишний. В каждом потоке хранится по одному обьекгу класса 51пц)етоп, и приложение погружается в хаос.
И это только пана из возможных ситуаций! А что будет, если к синглтону имеют лоступ несколько потокову (Представьте себе процесс отладки такой программы!) Опытные программисты, разрабатываюшие многопоточные приложения, узнают здесь классическое состязание (гасе з!шайоп), Следует быть готовым к тому, что шаблон проектирования 51пц1етоп столкнется с потоками. Синглтон относится к глобальным ресурсам совместного использования и должен участвовать в состязании с другими объектами, решая проблемы нескольких потоков.
б.й. 1. Шаблон блокировки с двойной лроваркой Всестороннее обсуждение многопоточных синглтонов впервые было проведено Дугласом Шмидтом (Ооой)аз Бслглк)г, 1996). В этой же статье было описано очень остроумное решение, названное шаблоном цоиЫе-Сбескеб ьос!слпц (блокировка с двойной проверкой), предложенное Дугласом Шмидтом и Тимом Харрисоном (Тпп НапЬоп). Очевилное решение сушествует, но выглядит непривлекательно. 5з'пц)егопб 5зпц)есоп::1пзгапсеО // юитех — объект мьютекса. // мьютексом управляет обьехт класса ьоск ьоск циагг((юитех ); )Г (!р1пзтапсе ) р1пзтапсе = пеа 51пц1етоп; гетигп *р1пзгапсе Класс ьоск -- это классический обработчик мьютексов (детали описаны в приложении).
Конструктор класса ьосх захватывает мьютекс, а деструктор освобождает его. Пока мьютекс витек захвачен, другие потоки, пытаюшиеся завладеть им, ожмлают своей очерели. Часть !!. Компоненты 168 Это освобождает нас от состязания: пока поток присвоен объекту р1пзтапсе, остальные останавливаются в конструкторе даат. Когда другой поток пытается захватить блокировку, он обнаруживает уже проинициализированную переменную ртпзтапсе, и все проходит гладко.
Однако правильное решение не всегда оказывается приемлемым. Неудобство заключается в недостатке эффективности. Каждый вызов функции-члена 1пзталсе влечет за собой блокировку и освобождение объекта синхронизации, хотя состязание за ресурсы возникает, образно говоря, один раз в жизни. Эти операции обычно очень затратны. Их стоимость намного превышает стоимость выполнения простой проверки з Т(! Р1пзтапсе), (В современных системах время, затрачиваемое на проверку и ветвление, обычно отличается от времени, уходяшего на блокировку критических разделов, на несколько порядков.) Для того чтобы избежать дополнительных затрат, можно было бы предложить следующее решение. 5з пд)еголин 5з од)етоп:: 1пзтапсе() ( зТ (!р1пзтапсе ) ( ьоск дцаго(витек ); р1пзтапсе = пеи 5зпд)етоп; гетцгп *р1пзтапсе ; Теперь дополнительных затрат нет, однако осгалось состязание за ресурсы.
Первый поток проходит проверку 1Ф, однако прямо перед входом в синхронизированную часть кода планировщик заданий операционной системы прерывает его и передает управление другому потоку. Второй поток также проходит проверку 11 (и, естественно, обнаруживает нулевой указатель), входит в синхронизированную часть кода и выполняет ее.