Perl — перекрестные ссылки и «утечка памяти»

Недавно столкнулся с такой не очевидной проблемой, как «утечка памяти» там, где вроде бы ее не должно было быть.

Маленькое вступлениеperl имеет механизм чистки уже неиспользуемых переменных. Работает просто — новая переменная всегда имеет счетчик ссылок, равный единице. Этот счетчик увеличивается на единицу, как только вы ставите на ту переменную ссылку или где-то используете эту переменую в функции, а саму функцию передаете куда либо еще — всё это отражается на счетчике. И пока этот счетчик не равен нулю — perl не освобождает память этой переменной. Счетчик также уменьшается на единицу, как только удаляется ссылка на переменную или, как во втором примере — удаляется ссылка на функцию, ее использующую, либо когда переменная становится не нужной после блока {}. Все это работает нормально, и сделано правильно, с точки зрения чистки памяти. Но бывают такие нюансы, о которых просто сходу не додумаешься — я баг искал несколько дней, и кое как нашел. Вообще, в большинстве случаев утечек памяти не будет — надо очень сильно постараться, чтобы они были, или писать программы в стиле 80-х годов, когда был Бейсик (perl позволяет писать в разных стилях) и о локальных переменных никто не думал.

Итак, рассмотрим несколько примеров, когда может быть утечка памяти, которую не так просто заметить:

{
 my $a = {};
 my $b = {};

 $a->{ref} = $b;
 $b->{ref} = $a;
}

Посмотрим, что здесь. Итак, $a — содержит ссылку на хеш-таблицу, $b — на другую. Все они — обернуты в блок. Казалось бы, $a & $b должны освободить память после окончания блока, так как после блока видимость переменных пропадает и в таких случаях perl все очищает. Тем более, ссылки на хеш-таблицы мы никуда не передавали и не возвращали через return. Но все будет не так. $a->{ref} получает ссылку на $b. Если бы этим все и ограничилось — память бы освободилась. Но проблема в том, что $b->{ref} сохраняет ссылку на $a, которая имеет ссылку на $b. Это и есть ключевой момент! Когда две переменные, как либо связаны через ссылки друг на друга (либо прямые ссылки, либо через хеши, как тут, либо через ссылки на функции, их использующие) — очистки памяти не происходит. И все потому, что perl не освобождает $a потому, что $b имеет ссылку на неё (тут счетчик ссылок для хеша под $a равен 2 внутри блока, для хеша под $b также равен 2 внутри блока), а $a имеет ссылку на $b. Когда блок заканчивается, perl уменьшает счетчики ссылок у обоих, и если в обычной ситуации все свелость бы к тому, что в результате у всех наступило бы по «нулям», то здесь у обоих счетчики ссылки так и остануться равными единицами. Выход — можно внутри блока в конце написать undef $b->{ref} (как бы разрубив гордиев узел), тем самым разрушим перекрестность ссылок.

Но есть пример и по интереснее, который приключился со мной. Он примерно, такой:

{
 my $ref = {};
 $ref->{func} = sub
  {
   ...
   $ref->{foo} = 2;
   ...
  }
}

Тут опять же — перекрестные ссылки, но более изощренно. $ref имеет в своем составе ссылку на функцию, например — callback по своей сути, а сам callback внутри себя использует $ref переменную. Выходит, что когда дело дойдет до чистки памяти — будет такая же ситуация — перекрестные ссылки и память не будет освобождена под $ref хеш, так и память переменных стека под sub {} (функции могут «удерживать» данные, которые им доступны из их «контекста» видимости и которые они используют). Как быть?

Второй пример решается одним способом, как ни странно (может есть и другой, с той же undef, но этот самый эффективный и «правильный»). Есть такой сервисный модуль — Scalar::Util с фунцией weaken. Код выше меняется на такой:

use Scalar::Util 'weaken';

{
 my $ref = {};
 weaken $ref;
 $ref->{func} = sub
  {
   ...
   $ref->{foo} = 2;
   ...
  }
}

Функция weaken управляет как раз тем счетчиком ссылок. Если говорить точно, то она перестает «удерживать» счетчик ссылок на тот объект, на который ссылается. А именно, в примере, $ref — удерживает без weaken счетчик на хеш — {}. После $ref = {} счетчик ссылок хеша равен двум — единице он был равен сразу после создания хеша, а затем еще увеличился на елиницу после присвоения $ref-у. Если мы используем weaken $ref, тогда после этого счетчик снова уменьшится на единицу для хеша, а внутри perl данных где-то в глубине это запомнится, и тогда в любой момент, как только perl очистит память под хеш (то есть когда счетчик ссылок станет нулевым), переменная $ref станет равной undef. Но, как правило, если правильно написать программу, возникновение значения undef для $ref нам не помешает. Тут главное, запомнить простое правило — если вы имеете ссылку на функцию, которая сама как либо использует ту переменную, которая ссылается на нее (прямо или опосредовано) — тут надо применять weaken, иначе будет утечка памяти! И это будет не вина perl, а ваша! Я в своей большой практике программирования на perl пока еще не сталкивался с утечкой памяти по вине perl — все было по вине программера, то есть меня 😉

Один комментарий к “Perl — перекрестные ссылки и «утечка памяти»”

Обсуждение закрыто.