Технология, използвана при ОупънСтеп и МакОС X

Технологията, използвана при програми за ОупънСтеп и МакОС X, представлява по същество комбинация между първия и четвъртия от описаните начини. И тук имаме брояч за всеки от обектите; указателите обаче са два вида — притежателни и непритежателни, а броячите отразяват само броя на притежателните указатели, които сочат към обекта. По такъв начин се решава проблемът с цикличните структури. Ако си мислим всеки от обектите като възли на някакъв граф, а всеки от указателите — като ребро, сочещо от един връх към друг, тогава разделянето на указателите на два вида води до дефинирането на определен подграф от целия граф, състоящ се единствено от онези от ребрата, които представляват притежателни указатели. Сега вече няма никакъв проблем целият граф да бъде цикличен; единственото, от което имаме нужда, е подграфът, съставен от притежателните указатели, да бъде ацикличен.

Тази технология е почти универсално приложима, поради което ще се спрем по-подробно на нея. Първоначално тя е била приложена с използването на езика Обджектив Си, който е и основният език за програмиране при ОупънСтеп и МакОС X. В днешно време обаче се забелязва тенденцията при програмиране на Обджектив Си да се използва събирач на боклука, при което работата за програмистите значително се опростява. Тук ще опишем тази технология, като я адаптираме към използване на езика Си++, който от една страна е значително по-популярен в сравнение с Обджектив Си, а от друга страна е до голяма степен неподходящ за използване на събирач на боклука.

Щом като всички обекти ще притежават броячи, ясно е че протоколът на обектите трябва да включва методи, предназначени за увеличаване и намаляване на броячите. Това са методите retain, с който броячът се увеличава, и release, с който броячът се намалява.

Кой обаче трябва да изпълнява тези методи и в какви случаи? По този въпрос важи просто правило:

При прилагането на това правило обаче, се натъкваме на следния проблем. Да предположим, че някой метод създава обект, след което трябва да го върне като стойност. Но щом го е създал, значи трябва да има грижата да го освободи. А ако го освободи, това ще доведе до унищожаването на обекта, така че той няма да може да бъде върнат като стойност.

За да решим този проблем, може да приемем, че при подобни случаи методът се освобождава от отговорността да освободи новосъздадения обект, като тази отговорност се прехвърля на обекта-получател. Подобна отмяна на универсалната приложимост на „златното правило“ обаче води след себе си до необходимостта това правило да бъде заменено от много на брой алтернативни правила, описващи отделни частни случаи. А където има сложност, там има и опасност от грешки. Майсторът-програмист се познава не по скоростта, с която генерира програмен код, а по умението му да намира прости решения на сложни проблеми.

За да решим възникналия проблем, ще добавим още един метод към протокола на всички обекти — autorelease. Можем да си мислим, че autorelease означава „намали брояча на обекта, но не веднага, а след известно време, когато това бъде безопасно“. По такъв начин методът, създал новия обект, може да изпълни autorelease и така хем да изпълни задължението си да освободи обекта, хем да не унищожи обекта, преди да го върне като стойност. Ако новосъздаденият обект е нужен на обекта-получател, то последният трябва да има грижата да обяви за това с retain.

Следва по-подробен сценарий, илюстриращ „собственическите“ взаимоотношения между обектите.

  1. Обект А получава например като аргумент на някой от методите си друг обект Б. А обаче не е собственик на Б, защото нито го е създал, нито си го е запазил с retain.

  2. А копира Б като по такъв начин получава нов обект В. Обектът В е създаден от А и затова А притежава В и трябва да има грижата да освободи В.

  3. А връща В като стойност на някой от методите си. Ако В не е нужен повече на А, А освобождава В с autorelease преди да го върне.

  4. Г получава В от А. Г може да направи каквото пожелае с В и след това да забрави за съществуването му. Г не притежава В и затова не отговаря за освобождаването му — след време обектът В ще бъде освободен автоматично тъй като е бил отбелязан с autorelease.

  5. Ако пък Г реши, че В му е нужен за по-дълъг прериод от време, тогава Г изпълнява retain. При това положение В няма да бъде унищожен преди Г да го освободи. Броячът на В засега е увеличен от 1 на 2, но след време когато заявеният autorelease се задейства, броячът ще се върне отново от 2 на 1, освен ако междувременно още някой друг обект не си го е запазил с retain.

При реализацията на метода autorelease се използват специални обекти-пулове, които съдържат указатели към обектите, получили съобщение autorelease. Във всеки момент от времето има активен пул. Обектите, получили съобщение autorelease, записват указател към себе си в активния в този момент пул. Когато в програмата се предизвика унищожаването на даден пул, всички обекти, записани в него, получават съобщение release. По този начин наистина може да гледаме на autorelease като на отложен release.

Най-общо казано интерактивните програми се изпълняват по следния начин:

while (true) {
    получаване команда от потребителя;
    изпълняване на тази команда;
}
        

Командите за създаване и унищожаване на активния пул се наместват тук по следния начин:

while (true) {
    // В началото на главния цикъл се създава нов пул.
    // Той автоматично става активен:
    AUTORELEASE_POOL *pool = new AUTORELEASE_POOL();

    // Всички обекти, които получат autorelease, се записват в pool:
    получаване команда от потребителя;
    изпълняване на тази команда;

    // В края на главния цикъл унищожаваме пула.  Това води до
    // изпращане на съобщение release до всички обекти, записани в пула.
    delete pool;
}
        

Интерактивните програми с графичен потребителски интерфейс също се изпълняват по описания начин, само че при тях главният цикъл не е част от програмата, а е скрит. Нормално програмата не е активна, операционната система я е спряла. Когато потребителят извърши дадено действие, например натисне бутон, операционната система извиква метода в програмата, който отговаря за обработката на това събитие. След като се предприемат необходимите действия, свързани с натискането на този бутон, методът, обработващ това събитие, завършва изпълнението си, с което програмата отново става неактивна — до следващото събитие от потребителя.

Но къде в тази схема се наместват командите за създаване и унищожаване на пула? Ако операционната система е МакОС X или програмираме за ОупънСтеп, самата среда ще се погрижи за това. В противен случай грижата е наша.

Да предположим, че събитието „натискане на бутон“ се обработва от метода button_pressed. В такъв случай добавяме нов метод button_pressed_responder със следната дефиниция:

void button_pressed_responder(някакви аргументи) {
    AUTORELEASE_POOL *pool = new AUTORELEASE_POOL();
    button_pressed_responder(същите аргументи);
    delete pool;
}
        

След това трябва да посочим, че събитието „натискане на бутон“ ще се обработва от button_pressed_responder вместо от button_pressed.

Създаваме нов метод, вместо да вмъкнем командите директно в button_pressed, защото така не замърсяваме button_pressed с команди, които нямат директно отношение към дейността му и биха попречили този метод да се извика от самата програма за емулиране натискането на бутона.

Пример 3.1. Използване на retain, release и autorelease в контейнерен клас.

Изглежда възможно най-простият контейнерен клас (т.е. клас, чиито обекти служат за съхранение на голямо количество други обекти) е стекът. Следва една проста реализация на стек, която използва retain, release и autorelease.

template <class T, int capacity = 1000>
class STACK : public OBJECT {
    public:
        STACK ();
        void push (T* data);
        T* peek ();
        T* pop ();
        unsigned size ();
    private:
        ~STACK ();                                      1
        T* storage [capacity];
        unsigned top;
};

template <class T, int capacity>
STACK<T, capacity>::STACK () 
        : OBJECT () {                                   2
    top = 0;
}

template <class T, int capacity>
void STACK<T, capacity>::push (T* data) {
    assert (size () < capacity);
    data->retain ();                                    3
    storage [top] = data;
    top++;
}

template <class T, int capacity>
    T* STACK<T, capacity>::peek () {
    assert (size () > 0);
    return storage [top - 1];                           4
}

template <class T, int capacity>
T* STACK<T, capacity>::pop () {
    assert (size () > 0);
    T* result = peek ();
    top--;
    result->autorelease ();                             5
    return result;
}

template <class T, int capacity>
unsigned STACK<T, capacity>::size () {
    return top;
}

template <class T, int capacity>
STACK<T, capacity>::~STACK () {
    for (unsigned u = 0; u < top; u++)
        storage [u]->release ();                        6
}
        
1

Деструкторът на стека, както и деструкторите на всички наследници на OBJECT са в секцията private:, за да се гарантира, че обектите ще се освобождават с retain, а не с delete.

2

Самият стек също има брояч и субконструкторът от OBJECT се грижи да го инициализира. Изричното извикване на субконструктора може и да се пропусне, защото компилаторът сам ще се погрижи да го извика.

3

Тъй като обектът data ще влезе в стека, си го запазваме с retain.

4

Връщаме елементът на върха на стека. Не го освобождаваме, защото той остава в стека. Получателят трябва да се погрижи да си го запази, ако му трябва.

5

Изваждаме елемента на върха на стека, като го връщаме като стойност. Тъй като той повече няма да трябва в стека, го освобождаваме. Правим това с autorelease, а не с release, защото в противен случай елементът може да бъде унищожен преди получателят му да успее да си го запази с retain.

6

Деструкторът не унищожава елементите в стека с delete, а само ги освобождава с release.

Пример 3.2. Използване на retain, release и autorelease при при извършването на различни операции върху обектите.

Да разгледаме следната задача. Функцията next_set при всяко свое извикване връща указател към обекти от клас SET. Тези обекти формират редица

a1, a2,...,ak.

Когато елементите от редицата свършат, next_set връща 0. За всеки два обекта x и y от клас SET са дефинирани операциите x+=y, връщаща void, и x<y, връщаща bool. Втората от тези две операции е транзитивна релация. Трябва да:

  1. намерим такъв елемент b от редицата, че за кой да е друг елемент a на същата редица, изразът b<a да има стойност false;

  2. пресметнем b+=ak+=ak-1+=+=a2+=a1.

За да решим тази задача, ще извикваме последователно функцията next_set и ще вкарваме получените обекти от клас SET в стек. Заедно с това ще сравняваме използвайки операцията < поредния получен елемент с избрания до този момент максимален елемент и ако трябва, ще изберем новия елемент за максимален.

Ето решението на задачата:

STACK <SET>* stack = new STACK <SET> ();                       1
SET* b = next_set ();                                          2
if (b != 0) {
    SET* next;
    b->retain ();                                              3
    stack->push (b);
    while ((next = next_set ()) != 0) {
        if (b < next) {
            b->autorelease ();                                 4
            b = next;
            b->retain ();
        }
        stack->push (next);                                    5
    }
    while (stack->size () > 0)
        b += stack->pop ();                                    6
    stack->release ();
}
          

1

Понеже ние създаваме обекта stack, ние сме му собственици и не е нужно да го запазваме с retain. След като си свършим работата със stack, трябва да имаме грижата да го освободим с release или с autorelease.

2

Обектът b не е създаден от нас, а е получен от next_set. Затова ако ни трябва, сме длъжни да го запазим с retain.

3

Запазваме b, защото на нас ни трябва. Това, че на следващия ред стекът също ще запази b нас не ни интересува.

4

И в тези три команди не се интересуваме какво стекът е запазил и какво не — не е наша работа да се интересуваме от това. Наша работа е да запазваме това, което на нас ни трябва, и да освобождаваме това, което повече на нас не ни трябва.

5

Нямаме грижа да запазим next не защото стекът ще се погрижи за това, а защото на нас next не ни трябва. Дали стекът ще запази next или не си е негова работа и нас не ни интересува.

6

Използваме това, което стекът ни е дал с pop, но то не ни трябва и за това нямаме грижа да го освобождаваме. В действителност стекът е изпълнил autorelease, което ще доведе до освобождаването на обекта, но нас това не ни интересува. Не ни интересува и това, че b може да си запази този обект, ако му трябва.

Да обърнем повече внимание на следния фрагмент, от тази задача:

b->autorelease ();
b = next;
b->retain ();
	

Защо освобождаваме b с autorelease, а не с release? Отговорът се крие в следното правило:

В нашия случай ако искаме да използваме release вместо autorelease, ще трябва да подредим командите по следния начин:

next->retain ();
b->release ();
b = next;
	

В противен случай е възможно освободения от нас обект да предизвика верижно унищожаване на много други обекти, включително и такива, които ни трябват.

Кой от тези два варианта е препоръчително да използваме — с release или autorelease?

Използването на release има предимството, че паметта ще бъде освободена веднага, щом се окаже непотребна. Това освобождаване обаче отнема време. Да си припомним кога се извършва то, когато се използва autorelease — чак когато командата на потребителя вече е изпълнена. Затова при интерактивни програми в общия случай използването на autorelease подобрява времето за реакция на програмата. Използването на autorelease има още едно важно предимство — не е нужно да мислим кога използването на release е възможно и кога не е. Но разбира се не бива да прекаляваме.