From: Игорь Гариев <gariev at hotmail dot com>
Date: Mon, 1 Nov 2007 18:21:07 +0000 (UTC)
Subject: Циклические ссылки и сборщик мусора в Perl
Оригинал: http://kiev.pm.org/?q=node/15
Улучшаем Perl код "уничтожением"
Автор статьй: Игорь Гариев(gariev at hotmail dot com)
Оригинал на английском: Better Code Through Destruction
Перевод выполнен tba
Ларри Уолл сказал, что перл позволяет делать простые вещи просто, а
сложные делает возможными. Перл хорош как для написания двухстрочных
скриптов, которые спасают мир в последнюю минуту (по крайней мере
спасают вас и ваш проект), так и для сложных проектов. Однако, хорошая
техника программирования на перл может сильно различаться в маленьких
и больших приложениях. Рассмотрим, например, сборщик мусора (garbage
collector) в перл. Он освобождает программиста от забот, связанных с
управлением памятью... до те пор, пока программист не создаст
циклические ссылки (circular references).
Сборщик мусора в перле считает ссылки. Когда счетчик достигнет нуля
(что означает, что никто не ссылается), перл уничтожает объект. Подход
прост и эффективен. Однако, при циклических ссылках (когда объект A
ссылается на объект B, а объект B ссылается на объект A) возникает
проблема. Даже если ничего в программе не будет ссылаться на A и B,
счетчик ссылок на них никогда не достигнет нуля. Объекты A и B не
уничтожатся. Если в коде они создаются снова и снова (возможно в
цикле), мы получим утечку памяти (memory leak). Количество памяти,
выделяемое программе увеличивается без разумных на то оснований и
никогда не сможет уменьшится. Такой эффект допустим в простом
выполнился-завершился скрипте, но не допустим в программах работающих
24x365, таких как mod_perl или FastCGI окружения, или как
самостоятельные серверы.
Циклические ссылки иногда очень полезны. Типичный пример - древовидная
структура данных. Для навигации в обоих направлениях - от корня к
листьям и наоборот - родительский узел хранит список дочерних, а
дочерний ссылается на родительский. Получаем циклические ссылки. Во
многих CPAN модулях модель данных реализована таким образом, включая
HTML::Tree, XML::DOM и Text::PDF::File. Все эти модули содержат метод
для освобождения памяти. Клиентское приложение должно вызвать этот
метод, когда объект перестает быть нужен. Однако требование явного
вызова не является изящным и может содержать небезопасный код:
###
## Код с утечкой памяти
#
use HTML::TreeBuilder;
foreach my $filename (@ARGV) {
my $tree = HTML::TreeBuilder->new;
$tree->parse_file($filename);
next unless $tree->look_down('_tag', 'img');
##
## Здесь происходит настоящя работа (например, извлечение изображений)
## ...
## и утечка памяти
##
$tree->delete;
}
Проблема в инструкции next: HTML документы без <img ... тэгов не
освобождаются. В действительности, любой вызов next, last, return
(внутри подпрограммы) или die (внутри eval {} блока) явлется
небезопасным и приводит к утечке памяти. Конечно, можно поместить
освобождающий код в блок continue для last или next, или писать код
для уничтожения дерева перед каждым return или die, но код быстро
становится перегруженным.
Существует лучшее решение - парадигма "за выделение ресурсов отвечает
инициализатор (а за освобождение - уничтожитель)" (в шутку вторая
половина часто опускается, даже когда она является, возможно, наиболее
важной). Идея проста. Создается специальный служебный объект (другого
класса), чья основная обязанность освобождать ресурс. Когда служебный
объект уничтожается, его деструктор удаляет дерево. Код может
выглядеть так:
##
## Применяется специальный служебный объект
##
use HTML::TreeBuilder;
foreach my $filename (@ARGV) {
my $tree = HTML::TreeBuilder->new;
$tree->parse_file($filename);
my $sentry = Sentry->new($tree);
next unless $tree->look_down('_tag', 'img');
##
## next, last or return are safe here.
## Tree will be deleted automatically.
##
}
package Sentry;
sub new {
my $class = shift;
my $tree = shift;
return bless {tree => $tree}, $class;
}
sub DESTROY {
my $self = shift;
$self->{tree}->delete;
}
Заметим, что в конце цикла нет необходимости явно вызывать
$tree->delete. Фокус прост. Когда в процессе выполнения программный
поток покидает блок, $setry уничтожается, потому что он не участвует в
циклических ссылках. Код метода DESTROY пакета Sentry, в свою очередь,
вызывает метод delete объекта $tree. Это решение для всех случаев;
память будет возвращена как только вы покидаете блок.
Наконец, нет неоходимости в написании своего класса Sentry. Используем
Object::Destroyer, написанный Адамом Кеннеди(Adam Kennedy). Как можно
догадаться и названия, это объект, который уничтожает другие объекты:
##
## Решение с использованием Object::Destroyer
##
use HTML::TreeBuilder;
use Object::Destroyer 2.0;
foreach my $filename (@ARGV) {
my $tree = HTML::TreeBuilder->new;
my $sentry = Object::Destroyer->new($tree, 'delete');
$tree->parse_file($filename);
next unless $tree->look_down('_tag', 'img');
##
## You can safely return, die, next or last here.
##
}
Поскольку название освобождающего метода может различаться в раных
модулях, оно идет вторым аргументом конструктора.
Наконец, можно уничтожить любую струтуру данных, не только объекты,
если обеспечить код для этого. Передадим ссылку на подпрограмму или
анонимную подпрограмму:
##
## Unblessed структура данных с циклическими ссылками,
## которая не может самостоятельно "распутаться"
##
use Object::Destroyer 2.0;
while (1) {
my (%a, %b);
$a{b} = %b;
$b{a} = %a;
my $sentry = Object::Destroyer->new( sub { undef $a{b} } );
}
Ради смеха закомментируйте строку с $sentry объектом и понаблюдайте за
потребляемой скриптом памятью.
Использование Object::Destroyer как враппера.
Object::Destroyer может также облегчить жизнь авторам модулей. Если вы
написали библиотеку с циклическими ссылками, то можно или попросить
пользователей явно вызывать методы удаления, или использовать новую
возможность перл (стабильна начиная с perl 5.8; см. Scalar::Util) -
слабые ссылки (weak references). Слабые ссылки не увеличивают счетчики
ссылок на объекты, поэтому сборщик мусора перл может безопасно
собирать ссылки. В примере с деревом все ссылки от листьев к родителям
(но не наоборот, иначе мы потеряем дерево) могут буть слабыми. Когда
последняя ссылка на корневой узел уничтожится, рекурсивно уничтожатся
и ссылки на все дочерние узлы. Их счетчики станут равными нулю и перл
освободит память, занимаемую всеми ветвями дерева.
Действительно, некоторые CPAN модули используют подобный подход
(XML::Twig). Однако такое решение работает только если слабые ссылки
доступны. Во-вторых это может требовать довольно большого повторения
кода (в коде XML::Twig 3.26 девять вызовов weaken).
В качестве альтернативы можно использовать Object::Destroyer во
внутренней библиотеке. Это может работать как почти прозрачный враппер
над объектом:
##
## Object::Destroyer как враппер
##
package My::Tree;
use Object::Destroyer 2.0;
sub new {
my $class = shift;
my $self = bless {}, $class;
$self->populate;
return Object::Destroyer->new( $self, 'release' );
}
sub release{
## действительное выделение памяти
}
sub say_hello{
my $self = shift;
print "Hello, I'm object of class ", ref($self), "n";
}
package main;
{
my $tree = My::Tree->new;
$tree->say_hello;
##
## $tree->release будет вызываться Object::Destroyer'ом;
##
}
Объект $tree в клиентском коде в действительности является объектом
Object::Destroyer, который пересылает все вызываемые методы внутренним
объектам класса My::Tree. Метод say_hello не видит никакой разницы -
он получает исходный $self объект. Изменения в коде минимальны и легко
отслеживаются.
Этот подход также имеет ограничения: объекты не должны обращаться к
атрибутам объекта напрямую (например, $tree->{age}). В любом случае
подобная практика - плохой стиль. В добавок, вызовы методов объекта
клиентским кодом приводят к небольшим задержкам по времени. Вызовы из
кода библиотеки не затрагиваются.
Исключения и освобождение ресурсов
Выделение ресурсов через инициализацию (RAII) - мощная техника для
управления различными критическими ресупсами, не только памятью. Она
особенно полезна при использовании с исключениями для обработки
ошибок. Эта связка деляет код довольно понятным: исключения разделяют
нормальную логику выполнения и обработку ошибок, а RAII гарантирует
правильное освобождение всех ресурсов.
Рассмотрим сигналы как пример. Предположим, что у нас имеется
некоторый потенциально долго выполняемый (или даже бесконечный) код.
Мы не хотим, чтобы скрипт подвис и хотим прервать его выполнение.
Сигналы как раз для таких задач. Однако первая же попытка написания
хорошего кода может быть неуклюжей:
##
## Пример сигнала 1. Интуитивный.
##
eval{
local $SIG{ALRM} = sub { die "Timed outn" };
alarm(5);
long_running_code();
## Отменяем сигнал, если код выполнится за 5 сек.
alarm(0);
};
Код будет работать пока в long_runnig_code() не выполнится die. В этом
случае блок eval поймает die, но не сигнал. Если это случится в
программе, которая должа работать 24 часа в сутки, программа
остановится через 5 секунд.
Следующий пример намного лучше; вообще-то это пример рабочего кода.
Этого достаточно для большинства приложений. Однако он не совсем
пуленепробиваем.
##
## Пример сигнала 2. Стандартное решение.
##
eval{
local $SIG{ALRM} = sub { die "Timed outn" };
alarm(5);
long_running_code();
## Отменяем сигнал, если код выполнится за 5 сек.
alarm(0);
};
## Отменяем сигнал, если long_running_code() "умирает".
alarm(0);
Сколько раз сигнал будет отменен в следующем коде?
##
## Пример сигнала 3. Коварный код.
##
LOOP:
foreach my $arg (1..3) {
eval{
local $SIG{ALRM} = sub { die "Timed outn" };
alarm(5);
long_running_code($arg);
alarm(0);
};
alarm(0);
}
sub long_running_code{ last LOOP; }
Упс, ни разу.
RAII решение более надежно:
##
## Пример сигнала 4.
## Ресурсы под контролем Object::Destroyer
##
eval{
local $SIG{ALRM} = sub { die "Timed outn" };
alarm(5);
my $sentry = Object::Destroyer->new( sub {alarm(0)} );
long_running_code();
};
Вне зависимости от того, каким образом завершится блок eval, перл
уничтожит объект $sentry. Уничтожение вызовет alarm(0).
Подобным образом мы можем управлять многими ресурсами, включая
файловые блокировки, семафоры и даже блокировки таблиц баз данных.
##
## Файловая блокировка.
##
use Fcntl ':flock';
open my($fh), ">$filename.lock";
eval{
flock($fh, LOCK_EX);
my $sentry = Object::Destroyer->new( sub {flock($fh, LOCK_UN)}
);
##
## Здесь действительный код.
## die безопасно.
##
};
##
## Семафор
##
use Thread::Semaphore;
use Object::Destroyer;
my $s = Thread::Semaphore->new();
eval{
$s->down;
my $sentry = Object::Destroyer->new( sub { $s->up } );
##
## Здесь критический код, die безопасно
##
};
##
## Блокировка таблицы MySQL
##
use DBI;
my $dbh = DBI->connect("dbi:mysql:...", "", "");
eval{
$dbh->do("LOCK TABLE table1 READ");
my $sentry = Object::Destroyer->new(
sub { $dbh->do("UNLOCK TABLES"); }
);
##
## Опять же основной код здесь
##
};
Код чистый, простой и довольно самодоументируемый.
Простые транзакции
Все, кто работал с реляционными базами данных знают насколько полезны
транзакции. Одной из особенностей транзаций является атомарность: или
все изменения данных происходят сразу, либо все игнорируются. Наши
данные всегда непротиворечивы; нет возможности оставить их в
промежуточном состоянии. Подобный эффект достижим и в перл:
eval {
my $coderef = create_savepoint($account1, $account2);
my $sentry = Object::Destroyer->new($coderef);
die "before changes" if rand > 0.7;
$account1 += 3;
die "after account 1 was modified" if rand > 0.7;
$account2 -= 3;
die "after account 2 was modified" if rand > 0.7;
##
## Здесь предполагается подтверждение транзакций
## и $sentry может быть освобождена.
## $coderef->() не может быть вызвана.
##
$sentry->dismiss;
die "after transaction is committed" if rand > 0.7;
};
sub create_savepoint {
## Save references to variables and their current values
my @vars;
foreach my $ref (@_) {
die "Can remember only scalar values" unless ref($ref) eq 'SCALAR';
push @vars, { ref => $ref, value => $$ref };
}
## Замыкание для восстановления их значений
return sub {
foreach my $var (@vars) {
${ $var->{ref} } = $var->{value};
}
};
}
Выполним скрипт несколько раз. Из-за rand он будет ломаться на
некоторой линии, но получить суммарное число отличное от 30
невозможно.
Полезные ссылки
RAII несомненно новая техника. Она очень популярна в мире C++. Если вы
не боитесь C++, вы можете найти интересным стандартный
контейнер auto_ptr и эффетивное использование auto_ptr.
Нестандартный класс ScopeGuard обеспечивает лексически определенное
управление памятью в C++.
В модуле Devel::Monitor можно найти примеры как проектировать
структуры данных с помощью слабых и циклических ссылок. Его
первоочередная цель, между прочим, - отслеживание потребления памяти
выполняющимся скриптом.
В CPAN есть также несколько модулей для лексически ограниченного
управления памятью, но Object::Destroyer мой любимый. Вы также
можете взглянуть на Hook::Scope, Scope::Guard и
Sub::ScopeFinalizer.
Наконец, в Object Oriented Exception Handling in Perl обсуждается,
почему исключения бесценны в больших проектах.
455 Прочтений • [Циклические ссылки и сборщик мусора в Perl (perl trash garbage)] [08.05.2012] [Комментариев: 0]