Утечки памяти в программах на Perl::Журнал СА 5.2004
www.samag.ru
Льготная подписка для студентов      
Поиск   
              
 www.samag.ru    Web  0 товаров , сумма 0 руб.
E-mail
Пароль  
 Запомнить меня
Регистрация | Забыли пароль?
О журнале
Журнал «БИТ»
Подписка
Где купить
Авторам
Рекламодателям
Магазин
Архив номеров
Вакансии
Контакты
   

Jobsora


  Опросы

Какие курсы вы бы выбрали для себя?  

Очные
Онлайновые
Платные
Бесплатные
Я и так все знаю

 Читать далее...

1001 и 1 книга  
28.05.2019г.
Просмотров: 1943
Комментарии: 2
Анализ вредоносных программ

 Читать далее...

28.05.2019г.
Просмотров: 1973
Комментарии: 1
Микросервисы и контейнеры Docker

 Читать далее...

28.05.2019г.
Просмотров: 1530
Комментарии: 0
Django 2 в примерах

 Читать далее...

28.05.2019г.
Просмотров: 1117
Комментарии: 0
Введение в анализ алгоритмов

 Читать далее...

27.03.2019г.
Просмотров: 1693
Комментарии: 1
Arduino Uno и Raspberry Pi 3: от схемотехники к интернету вещей

 Читать далее...

Друзья сайта  

Форум системных администраторов  

sysadmins.ru

Электронка - 2020!

 Утечки памяти в программах на Perl

Архив номеров / 2004 / Выпуск №5 (18) / Утечки памяти в программах на Perl

Рубрика: Программирование /  Анализ данных

АЛЕКСЕЙ МИЧУРИН

Утечки памяти в программах на Perl

Все программисты знают, сколько неприятностей могут доставить утечки памяти, как сложно бывает локализовать и устранить их причины. И любой, кто работал с Perl, смог по достоинству оценить систему сборки мусора, которой оснащён интерпретатор этого языка. Она автоматически заботится об удалении данных, которые больше не понадобятся, и защищает таким образом от утечек памяти. У программиста может появиться некая иллюзия неограниченной мощи этого механизма. Но так ли он всемогущ?

Perl – прекрасный язык (и мы сегодня в этом ещё убедимся), позволяющий программисту весьма кратко выразить достаточно ёмкие мысли. Приведу пример программы, состоящей из одной строки и демонстрирующей утечку памяти в Perl:

# ОСТОРОЖНО! УТЕЧКА ПАМЯТИ!

while (1) { my ($a,$b); $a=$b; $b=$a }

Если вы решите испытать эту нехитрую программу, то приготовьтесь к тому, что события будут развиваться стремительно. На моей станции она «съедает» всю память за доли минуты.

В ней, как вы видите, реализован бесконечный цикл. В теле цикла созданы две локальные переменные $a и $b. Ожидается, что, как и положено локальным переменным, они будут уничтожаться каждый раз по окончании выполнения тела цикла, но этого не происходит. Почему?

Причина кроется в устройстве системы сборки мусора. Последняя просто ведёт учёт всех ссылок, сделанных на переменную. Если переменная вышла из области видимости, но ссылки на неё остались, то данные не удаляются (хотя имя переменной становится недоступным). Это обеспечивает работоспособность всех ссылок, но может ввести в заблуждение систему сборки мусора. Обратите внимание, ссылки не анализируются на предмет, будут они удалены в ближайшее время или не будут. Они подсчитываются все без исключения.

Иллюстрацией к сказанному может послужить абсолютно «бытовая» ситуация, встречающаяся сплошь и рядом:

my $ref;

{ my $var=1975; $ref=$var; }

# print $var # неверно, имя $var уже не доступно

print $$ref;

# будет напечатано "1975", данные не исчезли

Как видите, за пределами блока имя $var уже не доступно, но данные, ранее ассоциированные с этим именем, сохранились и будут в сохранности, пока не будет удалена последняя ссылка на них

В нашем однострочном примере переменные $a и $b содержат ссылки друг на друга. Несмотря на то, что их имена становятся не видны сразу по окончании выполнения блока, система сборки мусора не удаляет данные, ассоциированные с ними, неуклонно следуя своему алгоритму: данные считаются «мусором» только в том случае, если ссылок на них не осталось.

Может быть, кому-то покажется, что подобная ситуация попахивает надуманностью и встречается редко? Отнюдь. Например, у вас может быть массив структур[2], описывающих товары, и другой массив структур, описывающих магазины. Структуры, описывающие товары, могут содержать указатели на магазины, а структуры, описывающие магазины, могут ссылаться на элементы списка товаров. Вот мы и получили две конструкции, ссылающиеся друг на друга. Больше того, реляционные базы данных просто-таки сами подсказывают подобные решения.

Давайте разберёмся, как же решить проблему утилизации ненужных данных.

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

01: #!/usr/bin/perl -w

02:

03: # ОСТОРОЖНО! УТЕЧКА ПАМЯТИ!

04:

05: sub create_ring {

06:     my ($length)=@_;

07:     my @ring=({"value" => 0})x$length;

08:     for (my $i=0; $i<$length; $i++) {

09:         $ring[$i]{"next_ref"}=$ring[($i+1)%$length];

10:     }

11:     return @ring;

12: }

13:

14: while (1) { my @ring=create_ring(10) }

Процедура create_ring создаёт и возвращает массив хэшей, каждый из которых имеет два ключа. Под именем «value» хранится некая величина, под именем «next_ref» – ссылка на следующий элемент массива. Последний элемент несёт ссылку на первый элемент (номер ноль), зацикливая конструкцию.

Вызывая её в бесконечном цикле и следя за процессами в системе, легко убедиться, что в приведённой программе есть утечка памяти. Элементы списка, создаваемого create_ring, не удаляются по той же причине, что и $a и $b из первого примера: когда выполнение блока доходит до конца, остаются неудалённые ссылки на элементы списка (как вы помните, эти ссылки содержатся в хэшах, составляющих список, под именами «next_ref»).

Ситуация не изменится даже в том случае, если чуть изменить наш код (привожу только важные и изменённые строки):

...

03: # ОСТОРОЖНО! УТЕЧКА ПАМЯТИ!

...

11:     return $ring[0];

...

14: while (1) { my $ring=create_ring(10) }

Не поможет и такое изменение:

...

03: # ОСТОРОЖНО! УТЕЧКА ПАМЯТИ!

...

11:     return @ring;

...

14: while (1) { my $ring=create_ring(10) }

В двух последних примерах по окончании выполнения блока мы будем терять ссылку на элемент массива (в первом) или ссылку на сам массив (во втором), но данные массива, уже никому не доступные, будут бережно сохраняться системой сборки мусора.

Один из способов освободить память – разорвать этот круг (вернее, ring) вручную, добавив в конец блока соответствующую инструкцию:

...

14: while (1) {

15:     my $ring=create_ring(10);

16:     $ring->[0]{"next_ref"}=undef;

17: }

Одного разрыва вполне достаточно, чтобы система сборки мусора один за другим удалила все элементы структуры, но этот способ не выглядит изящно. Мы просто взяли на себя труд убрать мусор, но так и не заставили работать над этим интерпретатор Perl. Неужели всё так безнадёжно? Нет! В Perl есть механизмы, позволяющие «растолковать» ему все детали нашего замысла, я говорю об объектно-ориентированном подходе.

Первый пример будет основан на связывании переменной с классом

Я буду рассчитывать на то, что читатель знает об устройстве модулей Perl и знаком с механизмом связывания объектов с переменными. Вкратце скажу, что Perl позволяет ассоциировать переменную с классом с помощью оператора tie. После такого связывания переменная начинает жить по собственным законам, при любой манипуляции с ней вызываются соответствующие методы объекта, с которым она связана.

Вот код, реализующий обозначенный подход:

01: #!/usr/bin/perl -w

02:

03: package MyRing;

04:

05: sub create_ring {

06:     my ($length)=@_;

07:     my @ring=({"value" => 0})x$length;

08:     for (my $i=0; $i<$length; $i++) {

09:         $ring[$i]{"next_ref"}=$ring[($i+1)%$length];

10:     }

11:     return @ring;

12: }

13:

14: sub TIESCALAR {

15:     my ($class, $length)=@_;

16:     return bless create_ring($length), $class;

17: }

18:

19: sub FETCH { return $_[0]; }

20:

21: sub STORE { die "писать в MyRing нельзя. " }

22:

23: sub DESTROY {

24:     my ($self)=shift;

25:     $self->[0]{"next_ref"}=undef;

26: }

27:

28: package main;

29:

30: # демонстрация работоспособности

31: {

32:    my $a;

33:    {

34:       tie my $ring, "MyRing", 10;

35:       $ring->[1]{"value"}="test";

36:       $a=$ring;

37:    }

38:    # $ring больше не видна, но данные целы

39:    print $a->[0]{"next_ref"}{"value"}." ";

40:    # здесь вызывается DESTROY

41: }

42: # $ring=7; # это вызовет метод STORE

43:

44: while (1) { tie my $ring, "MyRing", 10; }

Для упрощения я не стал выделять модуль, описывающий класс, в отдельный файл. Связывать объект будем со скалярной переменной, это тоже, наверное, не лучшее решение: естественнее было бы выбрать массив, а если бы мы захотели сделать нашу разработку более масштабируемой и развиваемой, то логичнее было бы выбрать хэш. Но для связывания таких «сложных» конструкций нам пришлось бы реализовать множество методов, для скаляра же достаточно четырёх, эта компактность и наглядность лучше всего подойдёт для обсуждения в статье.

Итак, в строках 3-27 описан модуль MyRing. Он содержит уже знакомую нам функцию создания и инициализации массива хэшей (create_ring) и методы, необходимые для обеспечения работоспособности связанной переменной.

Метод TIESCALAR (строка 14) вызывается в момент связывания, он получает от оператора tie список дополнительных параметров, в котором у нас будет только одна величина – длина требуемого массива. Получив параметры, метод создаёт наш массив (вызов create_ring($length)), ассоциирует его с классом (вызов bless) и возвращает «новоиспечённый» объект.

Метод FETCH (строка 19) вызывается, когда выполняется чтение значения связанной переменной. Ему передаётся один параметр – сам объект. Наша реализация FETCH не делает ничего, просто возвращает то, что получила без изменений.

Метод STORE (строка 21) отвечает за запись в переменную. Я не придумал, что в него написать, он просто выдаёт грозное сообщение и «убивает» программу.

И наконец, метод DESTROY (строка 23) – это то, что нам нужно. Он вызывается автоматически всегда, когда переменная выходит за область видимости или становится недоступна по другим причинам (например, когда программа завершается). Наш метод DESTROY разрывает кольцевую структуру (строка 25 выглядит знакомо, не правда ли?), позволяя системе сборки мусора довести свою работу до конца.

Со строки 28 начинается основная программа, где мы сейчас и воспользуемся нашим классом MyRing.

В строках 30-42 приведён небольшой фрагмент кода, демонстрирующий работоспособность нашей «кухни».

В строке 32 мы создаём локальную переменную $a, которая будет существовать только в пределах блока, находящегося в строках 31-41. Во вложенном блоке (строки 33-37) мы создаём локальную переменную $ring, которую сразу же связываем с классом MyRing (строка 35). Для проверки корректности структуры $ring записываем строку «test» в её первый узел (строка 36). Для проверки корректности сборки мусора сохраняем копию $ring в $a. Что произойдёт, когда вложенный блок закончится? Оказывается, метод DESTROY не будет вызван! Это и понятно, мы сохранили указатель на нашу структуру (помните? $ring является указателем на массив) в переменной $a, а она всё ещё существует. Переменной $ring не стало, но пропало только имя, данные целы. Мы убеждаемся в этом в строке 39. Здесь же мы убеждаемся в корректности структуры $ring (теперь $a), получив доступ к первому узлу как к узлу, следующему после нулевого. А метод DESTROY будет вызван только тогда, когда исчезнет переменная $a. Таким образом, всё работает правильно.

Обратите внимание и на то, что при выполнении манипуляций с $ring (строка 35) вызывается метод FETCH, отвечающий за чтение переменной, а не STORE, ответственный за запись. Действительно, чтобы проделать операцию, описанную в строке 35, нам пришлось считать значение указателя $ring, а не записать его.

А вот строка 42 не случайно закомментирована. Операция присвоения автоматически вызывала бы метод STORE, а он бы аварийно остановил выполнение программы.

Наконец в строке 44 реализован всё тот же бесконечный цикл. На каждом проходе в его теле вновь создаётся локальная переменная $ring, но теперь мы связываем её с классом MyRing. Благодаря этому по завершении блока Perl передаёт управление методу DESTROY, который корректно освобождает память.

Таким образом, нам больше не приходится удалять кольцевую структуру «руками». Мы научили систему сборки мусора обращаться с нашими данными, и Perl теперь сам (...почти) защищает нас от утечек памяти.

Я продемонстрировал предельно мудрёную схему, можно ли обойтись меньшей кровью? Конечно! В Perl грань объектно-ориентированного программирования весьма размыта. Взгляните на следующий код, в нём нет почти ничего, напоминающего о его объектно-ориентированности:

01: #!/usr/bin/perl -w

02:

03: package MyRing;

04:

05: sub main::create_ring {

06:     my ($length)=@_;

07:     my @ring=({"value" => 0})x$length;

08:     for (my $i=0; $i<$length; $i++) {

09:         $ring[$i]{"next_ref"}=$ring[($i+1)%$length];

10:     }

11:     return bless @ring, __PACKAGE__;

12: }

13:

14: sub DESTROY {

15:     my ($self)=@_;

16:     $self->[0]{"next_ref"}=undef;

17: }

18:

19: package main;

20:

21: while (1) { my $ring=create_ring(10); }

Обратите внимание, вызов create_ring в основной программе вообще ни чем не выдаёт объектно-ориентированную природу переменной $ring. Правда, саму create_ring нам пришлось чуть доработать, сделав её каким-никаким, а всё-таки конструктором (вызов bless в строке 11), и «экспортировав»[5] её в модуль main (имя main::create_ring в строке 5).

Метод DESTROY остался без изменений. Вызываться он будет в тех же случаях и обеспечит такие же функции, как и в предыдущем примере.

Я бы не стал говорить, что какой-то из двух приведённых методов решения проблемы лучше, а другой хуже. У каждого есть свои преимущества и недостатки. Первый длинноват, но код последователен и легко читается. Второй компактен, но менее универсален и его логику понять сложнее (вернее, сложнее разглядеть в нём объектно-ориентированный подход). Первый код работает чуть медленнее, поскольку переменная связана. Второй работает чуть быстрее, но допускает выполнение бессмысленных действий, например:

 $ring=7;

О плюсах и минусах можно спорить бесконечно. Кроме того, существует великое множество промежуточных подходов, сочетающих традиционное и объектно-ориентированное программирование в разных пропорциях. Приводить и обсуждать их все я, конечно, не буду, пусть каждый пишет код так, как привык.

Гораздо интереснее задать другой вопрос: насколько надёжна наша защита от утечек памяти? Не будем наивны, она тоже небезупречна и болеет теми же болезнями, что и однострочная программа из самого первого листинга.

Не будем далеко ходить за примером и чуть модифицируем тело цикла из последнего листинга:

...

21: # ОСТОРОЖНО! УТЕЧКА ПАМЯТИ!

22: while (1) {

23:     my $ring=create_ring(10);

24:     my $a;

25:     $ring->[0]{"value"}=$a;

26:     $a=$ring;

27: }

Утечка произошла по той же самой причине, что и обычно. Когда выполнение блока подходит к концу и наступает время собрать мусор, система сборки обнаруживает, что на кольцевую структуру ссылаются две переменных: $ring и $a. Вторую уничтожить не получается, потому что на неё имеется ссылка. Где эта ссылка находится, система сборки мусора уже не разбирается, но мы-то знаем, что она содержится в недрах структуры $ring. Круг замкнулся, система сборки мусора снова не заметила наш мусор.

Теперь, я надеюсь, читатель видит и причины возникновения проблемы, и пути её решения.

Одним словом, при создании сложных структур данных всегда надо помнить, что Perl не сможет угадать ваши мысли, и чётко представлять, каким образом он интерпретирует ваши команды.


Комментарии отсутствуют

Добавить комментарий

Комментарии могут оставлять только зарегистрированные пользователи

               Copyright © Системный администратор

Яндекс.Метрика
Tel.: (499) 277-12-41
Fax: (499) 277-12-45
E-mail: sa@samag.ru