Программист-прагматик. Путь от подмастерья к мастеру - Хант Эндрю. Страница 40
Балансировка и исключения
Языки, поддерживающие исключения, могут сделать процедуру освобождения ресурса нетривиальной. Как удостовериться, что все ресурсы, назначенные до возбуждения исключения, освобождены надлежащим образом? В некоторой степени ответ зависит от языка программирования.
Язык С++ поддерживает механизм исключений типа try…catch. К сожалению, это означает, что всегда существует по крайней мере два возможных варианта выхода из подпрограммы, которая перехватывает, а затем повторно возбуждает исключение:
void doSomething(void) {
Node *n = new Node;
try {
// do something
}
catch (…) {
delete n;
thow;
}
delete n;
}
Заметим, что созданный нами узел освобождается дважды – один раз во время нормального выхода из подпрограммы, а второй раз в обработчике исключений. Это явное нарушение принципа DRY и проблема в сопровождении, которая может возникнуть в любой момент.
Однако в наших интересах воспользоваться семантикой языка С++. Локальные объекты автоматически разрушаются при выходе из блока, в котором они находятся. Это дает нам несколько вариантов. Если обстоятельства позволяют, можно поменять n: оно обозначает не указатель, а реальный объект Node в стеке:
void doSomething1(void) {
Node n;
try {
// делаем что-либо
}
catch (…) {
throw;
}
}
В этом случае мы используем С++ для автоматического разрушения объекта Node независимо от того, возбуждено исключение или нет.
В случае, если замена указателя на объект невозможна, тот же самый эффект достигается при инкапсулировании ресурса (речь идет об указателе Node) в пределах другого класса.
// Класс оболочки для ресурсов Node
class NodeResource {
Node *n;
public:
NodeResource() {n = new Node;}
~NodeResource() {delete n;}
Node *operator ->() {return n;}
};
void doSomething2(void) {
NodeResource n;
try {
// do something
}
catch (…) {
throw;
}
}
Теперь класс-оболочка NodeResource выступает гарантом того, что при разрушении его объектов происходит и разрушение соответствующих узлов. Для удобства класс оболочка предоставляет оператор разыменования – », с тем чтобы пользователи могли обращаться к полям в инкапсулированном объекте Node напрямую.
Поскольку эта методика столь полезна, в стандартной библиотеке С++ имеется шаблонный класс autOjDtr, обеспечивающий автоматические оболочки для динамически размещаемых объектов.
void doSomething3(void) {
auto_ptr
// Обращение к узлу Node как р-»…
// В конце узел автоматически удаляется
}
В отличие от C++ язык Java реализует «ленивую» форму автоматического разрушения объекта. Объекты, ссылки на которые отсутствуют, считаются кандидатами на попадание в «мусор», и их метод finalize будет вызываться в любой момент, когда процедура сборки мусора будет претендовать на эти объекты. Представляя собой удобство для разработчиков, которым больше не приходится жаловаться на утечки памяти, в то же время он усложняет реализацию процедуры очистки ресурсов по схеме С + +. К счастью, разработчики языка Java глубокомысленно ввели компенсирующую языковую функцию – предложение finally. Если блок try содержит предложение finally, то часть программы, относящаяся к этому предложению, гарантированно исполняется только в том случае, если исполняется любая инструкция в блоке try. Неважно, возбуждается при этом исключение или нет (даже при выполнении оператора return программой в блоке try) – программа, относящаяся к предложению finally, будет выполнена. Это означает, что использование ресурса может быть сбалансировано с помощью программы типа:
public void doSomething() throws IOException {
File tmpFile = new File(tmpFileName);
FileWriter tmp = new FileWriter(tmpFile);
try {
// do some work
}
finally {
tmpFile.delete();
}
}
Подпрограмма использует промежуточный файл, который мы хотим удалить, независимо от того, как подпрограмма заканчивает свою работу. Блок finally позволяет нам выразить это в сжатой форме.
Случаи, при которых балансировка ресурсов невозможна
Возникают моменты, когда основная схема распределения ресурсов просто не годится. Обычно это происходит в программах, которые используют динамические структуры данных. Одна подпрограмма выделяет область в памяти и связывает ее в структуру большего размера, где она и находится в течение некоторого времени.
Хитрость здесь состоит в установлении семантического инварианта для выделения памяти. Необходимо решить, кто несет ответственность за данные в составной структуре. Что произойдет при освобождении структуры верхнего уровня? Есть три основных варианта развития событий:
1. Структура верхнего уровня также несет ответственность за освобождение любых входящих в нее подструктур. Затем эти структуры рекурсивно удалят данные, содержащиеся в них, и т. д.
2. Структура верхнего уровня просто освобождается. Любые структуры, на которые она указывает (и на которых нет других ссылок), становятся "осиротевшими".
3. Структура верхнего уровня отказывается освобождать себя, если в нее входят какие-либо подструктуры.
В этом случае выбор зависит от условий, в которых находится каждая взятая в отдельности структура данных. Однако этот выбор должен быть явным для каждого случая, и ваше решение должно реализовываться последовательно. Реализация любого из представленных вариантов на процедурном языке программирования типа С может представлять проблему: структуры данных сами по себе не являются активными. В этих условиях для каждой из основных структур предпочтительнее написать модуль, обеспечивающий стандартные средства распределения и освобождения. (Этот модуль также обеспечивает распечатку результатов отладки, преобразование в последовательную и параллельную формы и средства обхода.)
И наконец, если отслеживание ресурсов становится слишком хитрой процедурой, можно создать собственную форму ограниченной автоматической сборки «мусора», реализуя схему подсчета ссылок для ваших динамически распределенных объектов. В книге "More Effective С++" ([Меу9б]) этой теме посвящен целый раздел.
Проверка баланса
Поскольку прагматики не доверяют никому, включая авторов книги, то мы полагаем, что во всех случаях неплохо было бы написать такую программу, которая осуществляла бы реальную проверку того, освобождены ли ресурсы надлежащим образом. Для большинства приложений это обычно означает создание оболочек для каждого типа ресурса и их использование для отслеживания всех распределений и освобождений. В некоторых точках программы логика диктует, что ресурсы находятся в определенном состоянии; для проверки этого и необходимо использовать оболочки.
Например, в программе, выполняемой на протяжении длительного времени и обслуживающей запросы, наверняка есть одна-единственная точка в начале основного цикла обработки, в которой происходит ожидание прихода следующего запроса. Именно в этой точке можно получить подтверждение тому, что с момента последнего выполнения цикла использование ресурсов не увеличилось.
При работе на более низком (но не менее полезном) уровне можно потратиться на инструментальные средства, которые (помимо всего прочего) проверяют выполняемые программы на наличие утечек памяти (регулярного неосвобождения области памяти). Весьма популярными являются Purify (www.rational.com) и Insure++ (www.parasoft.com).