Анимация
JavaScript
|
Главная Библионтека Это освобождает нас от состязания: пока поток присвоен объекту plnstance , остальные останавливаются в конструкторе guard. Когда другой поток пытается захватить блокировку, он обнаруживает уже проинициализированную переменную plnstance , и все проходит гладко. Однако правильное решение не всегда оказывается приемлемым. Неудобство заключается в недостатке эффективности. Каждый вызов функции-члена instance влечет за собой блокировку и освобождение объекта синхронизации, хотя состязание за ресурсы возникает, образно говоря, один раз в жизни. Эти операции обычно очень затратны. Их стоимость намного превышает стоимость выполнения простой проверки ifClpinstance). (В современных системах время, затрачиваемое на проверку и ветвление, обычно отличается от времени, уходящего на блокировку критических разделов, на несколько порядков.) Для того чтобы избежать дополнительных затрат, можно было бы предложить следующее решение. Singleton* Singleton::lnstanceC) { if С!plnstance ) { Lock guardCniutex ); plnstance = new Singleton; return *plnstance ; Теперь дополнительных затрат нет, однако осталось состязание за ресурсы. Первый поток проходит проверку if, однако прямо перед входом в синхронизированную часть кода планировщик заданий операционной системы прерывает его и передает управление другому потоку. Второй поток также проходит проверку if (и, естественно, обнаруживает нулевой указатель), входит в синхронизированную часть кода и выполняет ее. После повторной активизации первый поток также входит в синхронизированную часть, но слишком поздно - снова создаются два объекта класса Si ngl eton. Кажется, что эта головоломка не имеет решения, но оказывается, что существует очень простое и элегантное решение. Оно называется шаблоном Double-Checked Locking (блокировка с двойной проверкой). Идея проста: проверяем условие, входим в синхронизированный код, а затем проверяем это условие снова. Указатель оказывается либо уже проинициализированным, либо нулевым. Ниже приведен код, позволяющий разобраться.и оценить преимущества шаблона блокировки с двойной проверкой. В нем действительно проявляется красота программирования. Singleton* Singleton::Instance С) { if CIplnstance) 1 { 2 Guard myGuardClock ); 3 if C!plnstance ) 4 plnstance = new Singleton; return *plnstance ; Допустим, что поток управления входит в зону неопределенности (строка 2). В эту точку могут войти сразу несколько потоков. Однако в синхронизированный раздел входит только один поток. В строке 3 неопределенности вообще нет. Здесь все ясно: указатель либо полностью проинициализирован, либо не проинициализирован вообще. Первый поток, который входит в этот раздел, инициализирует переменную, а все остальные не пройдут проверки на строке 4 и ничего не создадут. Первая проверка быстрая и фубая. Если объект класса Singleton доступен, вы его получаете. Если нет, необходимы дальнейщие исследования. Вторая проверка медленна и точна: она сообщает, действительно ли проинициализирован синглтон, или это должен сделать поток. В этом и заключается суть блокировки с двойной проверкой. Теперь мы получаем больщое преимущество: скорость доступа к синглтону высока настолько, насколько позволяет сам объект. Однако во время создания объекта класса singleton состязание больще не возникает. И все же... Профаммисты, имеющие очень больщой опыт работы с потоками, знают, что даже щаблон блокировки с двойной проверкой, правильный на бумаге, не всегда хоро-що работает на практике. В некоторых симмефичных мультипроцессорных системах (с так называемой релаксированной моделью памяти) порции информации зафужа-ются в память одновременно, а не последовательно. Эти порции записываются по возрастанию адресов, а не в хронологическом порядке. Из-за такого способа упорядочения записей память, просматриваемая одним процессором, может выглядеть так, будто порядок операций, выполненных другим процессором, был неправильным. Например, присваивание значения переменной pinstance может выполняться до за-верщения полной инициализации объекта класса Singleton! Таким образом, увы, щаблон блокировки с двойной проверкой оказался непригодным для таких систем. В заключение следует заметить, что перед реализацией щаблона Double-Checked Locking нужно просмотреть документацию компилятора. (Тогда его можно назвать щаб-лоном Triple-Checked Locking (блокировка с тремя проверками).) Обычно операционная система предоставляет альтернативные, мащинно-зависимые средства рещения проблемы параллелизма, например, барьеры, упорядочивающие доступ к памяти (memory barriers). По крайней мере, поставьте перед переменной pinstance спецификатор volatile. Хороший компилятор должен генерировать правильный код вокруг таких объектов. 6.10. Сборка В этой главе обсуждаются разные реализации класса Singleton, выявляются их относительные преимущества и недостатки. Не следует думать, что в результате мы сможем прийти к универсальному решению, поскольку каждая задача предъявляет к реализации класса Singleton свои фебования. Шаблонный класс SingletonHolder, определенный в библиотеке Loki, представляет собой контейнер для синглтонов, позволяюший применить шаблон проектирования Singleton. Следуя шаблону проектирования, основанному на применении стратегий (глава 1), класс SingletonHolder разработан как специализированный контейнер для объектов класса singleton, определенного пользователем. При использовании класса SingletonHolder профаммист получает все необходимые функциональные возможности и может создавать свой собственный код. В крайнем случае придется все переделать заново (поэтому этот случай и называют крайним). В этой главе рассмотрено несколько тем, практически не связанных друг с другом. Как же теперь реализовать класс Singleton, не раздувая размер профаммы? Для этого нужно тщательно разложить щаблон Singleton на стратегаи, как показано в главе 1. Разложив класс SingletonHolder на несколько стратегий, можно реализовать все варианты, рассмотренные выще, с помощью кода, состоящего из небольшого количества строк. Используя конкретизацию шаблонов, можно отобрать желательные свойства и пренебречь ненужными. Это очень важно: реализация класса Singleton не универсальна. Используются лишь те свойства, которые в итоге будут включены в сгенерированный код. Кроме того, реализация оставляет возможности для изменения и расширений. 6.10.1. Разложение класса SingletonHolder на стратегии Начнем с выделения стратегий из описанной выше реализации. В ней можно идентифицировать вопросы, связанные с созданием объекта, его продолжительностью жизни и потоками. Это три наиболее важных момента в разработке синглтонов. Рассмотрим соответствующие стратегии. 1. Стратегия Creation. Синглтон можно создать разными способами. Обычно для создания объекта стратегия Creation использует оператор new. Выделение процесса создания объекта в виде отдельной стратегии существенно, поскольку это позволяет создавать полиморфные объекты. 2. Стратегия Lifetime. Различаются следующие стратегии продолжительности жизни объектов. 2.1. Правила языка С++ - последним создан, первым уничтожен. 2.2. Феникс. 2.3. Синглтон с заданной продолжительностью жизни. 2.4. Бессмертный синглтон (объект, который никогда не уничтожается). 3. Стратегия ThreadingModel. Синглтон может работать в режиме одного потока, в стандартном многопоточном режиме (с мьютексами и блокировкой с двойной проверкой) или использовать платформно-зависимую потоковую модель. Все реализации класса singleton должны гарантировать уникальность объекта. Это условие не влияет на выбор стратегии, поскольку его невыполнение нарушает определение синглтона. 6.10.2. Требования, предъявляемые к стратегиям класса SingletonHolder Определим необходимые офаничения, накладываемые классом SingletonHolder на свои стратегии. Сфатегия Creation должна создавать и уничтожать объекты, следовательно, она должна содержать две соответствующие функции. Таким образом, если класс Creator<T> согласован со сфатегаей Creation, он должен содержать вьвовы следующих функций. т* pObj = Creator<T>::CreateC); Creator<T>::DestroyCpObj); Обратите внимание на то, что функции Create и Destroy должны быть статическими членами класса Creator. Класс Singleton не содержит объект, имеющий тип creator, - это не позволило бы решить проблемы, связанные с его продолжительностью жизни. Стратегия Lifetime, по существу, должна планировать разрушение объекта класса Singleton, созданного стратегией Creation. Функциональные возможности стратегии Lifetime выражаются в ее способности разрушать объект класса Singleton в заданный момент времени. Кроме того, стратегия Lifetime решает, какие действия 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 [ 51 ] 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |