Анимация
JavaScript
|
Главная Библионтека достичь, если интеллектуальный указатель будет возвращать объект-заместитель (proxy object), а не простой указатель. Конструктор объекта-заместителя захватывает объект, на который ссылается интеллектуальный указатель, а его деструктор освобождает его. Этот способ программирования описан в книге Страуструпа (Stroustrup, 2000). Ниже приводится код, иллюстрирующий этот подход. Во-первых, рассмотрим класс widget, имеющий две конструкции для захвата объекта: функции-члены Lock и Unlock. После вызова функции Lock открывается безопасный доступ к объекту, при этом вызов такой функции остальными потоками блокируется. После вызова функции Unl оск объект может поступать в распоряжение остальных потоков. class Widget { void LockC); void UnlockO; Затем определим шаблонный класс LockingProxy. Он предназначен для захвата объекта (с помощью описанного выше механизма Lock/unlock) на время существования объекта класса LockingProxy. template <class т> class LockingProxy { public: LockingProxy(T* pObj) : pointee (pObj) { pointee ->Lock(); } ~LockingProxy(); { pointee ->Unlock(); } T* operator->() const { return pointee ; } private: LockingProxy* operator=(const LockingProxy*); T* pointee; Кроме конструктора и деструктора в классе LockingProxy определен оператор ->, возвращающий указатель на объект. Несмотря на то что класс LockingProxy напоминает интеллектуальный указатель, вокруг него существует еше одна оболочка - сам шаблонный класс SmartPtr. template <class т> class SmartPtr { Lockingproxy<T> operator->() const; { return Lockingproxy<T>(pointee ); } private: T* pointee ; Напомним, что в разделе 7.3, посвященном механике оператора ->, уже указывалось, что компилятор может применять оператор -> к одному и тому же выражению несколько раз, пока не обнаружит простой указатель. Теперь представьте себе следу-щий вызов (считая, что в классе Widget определена функция DoSomething). SmartPtr<widget> sp = ...; sp->DoSomething(); Здесь кроется один нюанс: оператор -> класса SmartPtr возвращает временный объект класа LockingРгоху<т>. Компилятор продолжает применять оператор ->. Оператор -> объекта класса LockingРгоху<т> возвращает объект, имеющий тип widget*. Компилятор использует этот указатель на объект класса widget для вызова функции DoSomethi ng. Во время вызова этой функции объект класса Locking Ргоху<т> продолжает существовать и владеть объектом, находящимся в полной безопасности. После возврата управления из функции DoSomethi ng временный объект класса LockingРгоху<т> уничтожается, и объект класса widget освобождается. Автоматический захват объектов - хорошая иллюстрация расслоения интеллектуальных указателей, которое можно осуществить, внеся соответствующие изменения в стратегию Storage. 7.13.2. Многопоточность на уровне регистрации данных Иногда, кроме объектов, на которые они ссьшаются, интеллектуальные указатели манипулируют дополнительными данными. Как указывалось в разделе 7.5, несколько интеллектуальных указателей с подсчетом ссылок скрытно используют один и тот же счетчик. Если скопировать такой указатель из одного потока в другой, возникнут два интеллектуальных указателя, использующих один и тот же счетчик ссьшок. Разумеется, они оба продолжают указывать на тот же объект, но пользователю известно, какой из этих указателей в данный момент владеет им. В то же время счетчик ссылок пользователю не доступен и управляется исключительно интеллектуальным указателем. Для многопоточной среды опасность представляют не только интеллектуальные указатели с подсчетом ссылок. Интеллектуальные указатели с отслеживанием ссылок (раздел 7.5.4) содержат указатели друг на друга, которые также относятся к совместно используемым данным. Связывание ссылок объединяет интеллектуальные указатели в группу, причем они не обязаны принадлежать одному и тому же потоку. Следовательно, каждый раз, когда выполняется копирование, присваивание или уничтожение интеллектуального указателя с отслеживанием ссылок, нужно решать вопрос о захвате объекта, в противном случае дважды связанный список может оказаться разрушенным. В заключение отметим, что вопросы, связанные с многопоточностью, в итоге влияют на реализацию интеллектуальных указателей. В качестве примера рассмотрим, как многопоточность влияет на подчет ссылок и их связывание. 7.13.2.1. Многопоточный подсчет ссылок При копировании интеллектуальных указателей из разных потоков счетчик ссылок в разных потоках увеличивается непредсказуемым образом. Как показано в приложении, операция инкрементации не является атомарной. Для инкрементации и декрементации целых чисел в многопоточной среде нужно использовать тип ThreadingMode1<T>: :intType, а также функции Atomiclncrement и Atomi cDecrement. Это все немного усложняет. Вернее, ситуация осложняется, если мы хотим сделать подсчет ссылок независимым от потоков. Следуя принципам разработки классов на основе стратегий, разложим класс на элементы, описывающие его поведение, и свяжем с каждым из них соответствующий шаблонный параметр. В идеале класс SmartPtr задавал бы стратегии Ownership и ThreadingModel, применяя их для корректной реализации. Однако при подсчете ссылок в многопоточной среде все намного запутаннее. Например, счетчик должен иметь тип ThreadingModel<T>:: intType. Следовательно, вместо использования операторов ++ и - приходится применять функции Atomicln-crement и AtomicDecrement. Потоки и счетчик ссылок сливаются в одно целое, их невероятно трудно разъединить. Лучше всего включить многопоточность в стратегию Ownership. В этом случае можно получить две реализации: Ref Counting и RefCountingMT. 7.13.2.2. Многопоточное связывание ссылок Рассмотрим деструктор интеллектуального указателя со связыванием ссылок. template <class т> class SmartPtr { public: -SmartPtrC) { if Cprev == next ) { delete pointee ; else { prev ->next = next ; next ->prev = prev ; private: T* pointee ; SmartPtr* prev ; SmartPtr* next ; Деструктор выполняет классическое удаление элемента из дважды связанного списка. Для упрощения и ускорения реализации список сделан кольцевым - последний узел ссылается на первый. Это позволяет не проверять, являются ли указатели prev и next нулевыми. В кольцевом списке, состоящем из одного элемента, указатели prev и next будут равны указателю this. Если несколько потоков уничтожают интеллектуальные указатели, связанные друг с другом, деструктор, очевидно, должен быть атомарным (т.е. его работу не могут прервать другие потоки). В противном случае работу деструктора класса SmartPtr сможет прервать любой поток. Например, это может произойти в момент между обновлением указателей prev и next . В этом случае поток, прервавший работу деструктора, будет оперировать разрушенным списком. Аналогичные рассуждения касаются и конструктора копирования, и оператора присваивания класса SmartPtr. Эти функции должны быть атомарными, поскольку они манипулируют со списком владения. Интересно, что здесь нельзя применить семантику захвата объектов. В приложении, приведенном в конце книги, стратегии захвата разделены на стратегии уровня классов (class level strategies) и стратегии уровня объектов (object-level strategies). Операции захвата на уровне классов захватывают все объекты данного класса. В то же время операции захвата на уровне объектов относятся только к одному объекту. В первом случае тратится меньше памяти (только один мьютекс на класс), но возникают проблемы с производительностью программы. Второй случай (один мьютекс на объект) сложнее, но позволяет создавать реализации, которые работают быстрее. 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 |