Во всех языках программирования с автоматическим управлением памятью используется, в том или ином виде, сборщик мусора. И 1С тут не является исключением.
Работа простейшего сборщика мусора основана на подсчете количества ссылок на объект. Счетчик ссылок показывает, сколько других объектов ссылаются на этот объект. Если счетчик больше 0, то сборщик мусора считает, что этот объект еще кому-то нужен. Если счетчик ссылок становится равным 0, сборщик мусора удаляет этот объект из памяти (возможно, не сразу).
Использование такого алгоритма порождает проблему циклических ссылок. Если объекты А и Б прямо или косвенно ссылаются друг на друга, или объект ссылается сам на себя, образуется ссылочный цикл. У объектов, участвующих в ссылочном цикле, счетчик ссылок никогда не достигнет нуля. Сборщик мусора не удалит эти объекты из памяти и они будут занимать память до завершения процесса. Таким образом, циклические ссылки порождают утечку памяти.
Тем не менее, специфика решаемой задачи может требовать от программиста создания циклических ссылок. Поэтому во всех современных языках программирования проблема циклических ссылок тем или иным способом решается. Специальные алгоритмы сборки мусора, учитывающие циклические ссылки, есть в Java, JavaScript, C#, Python, Ruby, PHP, Go, Lua...
Вы видите в этом списке 1С? Правильно, потому что ее там нет. Наверное, 1С последний из языков, в котором эта проблема до сих пор не решена.
Вендор признает наличие проблемы, об этом даже есть статья на ИТС. Но решать ее не торопится.
Поэтому, как всегда, приходится все делать самому.
Всё нижеследующее является просто упражнением для ума. Пока не берусь утверждать, что это является хорошим промышленным решением, ввиду недостаточной практики применения. Но на тестах работает.
Существует два основных способа борьбы с циклическими ссылками.
1. Специальный алгоритм сборщика мусора, умеющий отслеживать изолированные группы объектов, ссылающиеся друг на друга, но недоступные для всех остальных объектов.
2. Использование "слабых" ссылок (weak references).
Мы никак не можем модифицировать алгоритм работы сборщика мусора в 1С, поэтому про 1-й вариант можно сразу забыть. А вот ко 2-му стоит присмотреться.
Что такое слабые ссылки и чем они отличаются от сильных?
Как уже говорилось выше, сильная ссылка на объект увеличивает его счетчик ссылок. Это гарантирует, что сборщик мусора не удалит объект из памяти, пока есть хоть одна сильная ссылка на этот объект. Именно сильные ссылки порождают проблему циклических ссылок и связанных с этим утечек памяти.
Слабая ссылка не увеличивает счетчик ссылок на объект. Таким образом, объект, на который есть только слабые ссылки, является потенциальной жертвой сборщика мусора. В принципе, он может быть удален в любой момент, и отсюда следует важное свойство слабой ссылки: существование объекта по этой ссылке не гарантируется!
Однако, поскольку сборка мусора выполняется с некоторыми интервалами, объект все же будет существовать в памяти какое-то время. Это время зависит от конкретной реализации сборщика мусора, а так же от от вида слабой ссылки. Например, в Java есть WeakReference, объекты которых удаляются при любой очередной сборке мусора, а есть SoftReference - эти могут пережить много циклов сборки мусора и будут удалены только при серьезной нехватке памяти.
Всё, что должна сделать функция, владеющая слабой ссылкой - это успеть получить сильную ссылку на объект до того, как он будет уничтожен. После этого она может заниматься чем угодно и сколь угодно долго: пока существует сильная ссылка, объект никуда не денется. По возможности, сильная ссылка должна оставаться в контексте этой функции, и не выходить за его пределы, иначе есть риск снова соорудить циклическую зависимость.
Реакция программы на отсутствие объекта по слабой ссылке остается на усмотрение программиста. Алгоритм должен предусматривать либо возможность восстановления объекта в памяти в любой момент (в консистентном состоянии), либо бросание исключения.
Подробнее о слабых ссылках и техниках их использования можно прочитать в сети.
Теперь посмотрим, какие возможности у нас имеются в 1С.
В нашем распоряжении есть кэш повторно используемых возвращаемых значений. В нем кэшируются возвращаемые значения функций, объявленных в общих модулях с режимом повторного использования.
ИТС сообщает нам, что в режиме повторного использования "На время сеанса" сохраненные значения будут удалены:
- на сервере, в толстом клиенте, во внешнем соединении, в тонком клиенте и в веб-клиенте с обычной скоростью соединения - через 20 минут после вычисления сохраняемого значения или через 6 минут после последнего использования;
- в тонком клиенте и веб-клиенте с низкой скоростью соединения - через 20 минут после вычисления сохраняемого значения;
- при нехватке оперативной памяти в рабочем процессе сервера;
- при перезапуске рабочего процесса;
- при переключении клиента на другой рабочий процесс.
Итак, значение может жить в кэше до 20 минут (6 минут в случае неиспользования), либо до ситуации нехватки памяти, либо до завершения процесса. Всё это очень похоже на свойства слабых ссылок!
Попробуем реализовать механизм слабых ссылок в 1С при помощи кэша повторно используемых значений.
Для начала определим некоторые термины:
- Объект - это любой объект, на который мы хотим ссылаться через слабую ссылку.
- Ключ - так будем называть слабую ссылку (чтобы не путать с термином Ссылка, который в 1С означает другое). В роли ключа будем использовать УникальныйИдентификатор.
- Контейнер - простейший объект 1С, внутри которого будет содержаться наш объект, на который указывает слабая ссылка. Для чего он нужен - объясню ниже. В роли контейнера будем использовать Массив из одного элемента.
Нам понадобится 2 общих модуля. Я делал серверную реализацию, но, в принципе, не вижу препятствий для аналогичной реализации на клиенте.
Начнем с модуля с повторным использованием. В нем будет всего одна функция, задача которой - возвращать пустой контейнер для заданного ключа.
#Область СлужебныйПрограммныйИнтерфейс
// Получить контейнер.
//
// Параметры:
// Ключ - Строка
//
// Возвращаемое значение:
// Массив из Произвольный
//
Функция ПолучитьКонтейнер(Знач Ключ) Экспорт
Возврат Новый Массив(1);
КонецФункции
#КонецОбласти
Для чего нам нужен контейнер?
Мы не можем добавить в кэш пару "ключ - объект" в готовом виде. Нужна функция, которая в параметрах получает ключ, а возвращает объект. Но ей неоткуда взять объект, ведь в параметрах его нет. Поэтому функция возвращает пустой контейнер - массив, ссылка на который кэшируется. А дальше мы используем этот контейнер для хранения ссылки на объект.
И теперь создадим фронтальный общий модуль, реализующий программный интерфейс.
#Область ПрограммныйИнтерфейс
// Получить ключ объекта (слабую ссылку).
//
// Параметры:
// Объект - Произвольный
// Ключ -
// - Неопределено - Создать новый уникальный ключ
// - УникальныйИдентификатор - Использовать переданный ключ
//
// Возвращаемое значение:
// УникальныйИдентификатор
//
Функция ПолучитьКлюч(Знач Объект, Знач Ключ = Неопределено) Экспорт
Если Ключ = Неопределено Тогда
Ключ = Новый УникальныйИдентификатор();
КонецЕсли;
Контейнер = СлабыеСсылкиПовтИсп.ПолучитьКонтейнер(Строка(Ключ));
Контейнер[0] = Объект;
Возврат Ключ;
КонецФункции
// Получить объект по ключу.
//
// Параметры:
// Ключ - УникальныйИдентификатор
// ВызыватьИсключение - Булево - Вызывать исключение, если объект не существует
//
// Возвращаемое значение:
// - Произвольный - Существующий объект
// - Неопределено - Если объект уже не существует
//
Функция ПолучитьОбъект(Знач Ключ, Знач ВызыватьИсключение = Истина) Экспорт
Объект = СлабыеСсылкиПовтИсп.ПолучитьКонтейнер(Строка(Ключ))[0];
Если Объект = Неопределено И ВызыватьИсключение Тогда
ВызватьИсключение "Объект, на который указывает слабая ссылка, был удален из памяти";
КонецЕсли;
Возврат Объект;
КонецФункции
#КонецОбласти
Функция ПолучитьКлюч создает слабую ссылку на объект. 2-й параметр позволяет передать уже имеющийся ключ - важно, чтобы это был ключ, ранее полученный именно для этого объекта! Вызов со 2-м параметром можно использовать, чтобы "взбодрить" кэш и продлить время жизни объекта еще на 6 минут.
Функция ПолучитьОбъект используется для получения объекта по слабой ссылке. Если кэш уже успел сброситься, то функция ПолучитьКонтейнер снова выполнится и вернет пустой контейнер. В этом случае по умолчанию вызывается исключение. Если нужно самостоятельно обработать ситуацию отсутствия объекта, то 2-м параметром следует передать Ложь.
Рассмотрим использование слабых ссылок на примере.
Есть две обработки, назовем их БазовыйОбъект и СпециальныйОбъект, которые вызывают методы друг друга. БазовыйОбъект реализует некую общую логику, но в рамках этой общей логики он вызывает callback-методы своего объекта-владельца СпециальныйОбъект, в котором реализована некоторая специфическая логика.
Экземпляр СпециальныйОбъект должен хранить ссылку на экземпляр БазовыйОбъект, а тот, в свою очередь, должен хранить ссылку на экземпляр СпециальныйОбъект. При использовании сильных ссылок образуется циклическая зависимость, в результате чего оба объекта станут бессмертными. Чтобы этого избежать, БазовыйОбъект будет хранить слабую ссылку на СпециальныйОбъект.
Перем М_КлючВладельца; // слабая ссылка на владельца
Функция ОбщийАлгоритм() Экспорт
// в самом начале получает сильную ссылку на владельца,
// гарантируя его выживание на все время выполнения функции
Владелец = СлабыеСсылки.ПолучитьОбъект(М_КлючВладельца);
// ...делает что-то общее
// здесь нужно вызвать владельца
Результат = Владелец.СпециальныйАлгоритм();
// ...
КонецФункции
Функция Конструктор(Знач КлючВладельца) Экспорт
М_КлючВладельца = КлючВладельца;
Возврат ЭтотОбъект;
КонецФункции
Перем М_КлючЭтого; // слабая ссылка на самого себя для передачи базовому объекту
Перем М_Базовый; // сильная ссылка на базовый объект
Функция ОбщийАлгоритм() Экспорт
Возврат Базовый().ОбщийАлгоритм();
КонецФункции
Функция СпециальныйАлгоритм() Экспорт
// ...делает что-то специфическое
КонецФункции
Функция Конструктор() Экспорт
// создает экземпляр базового объекта и передает слабую ссылку на себя
М_Базовый = Обработки.БазовыйОбъект.Создать().Конструктор(М_КлючЭтого);
Возврат ЭтотОбъект;
КонецФункции
Функция Базовый()
СлабыеСсылки.ПолучитьКлюч(ЭтотОбъект, М_КлючЭтого); // продлевает жизнь слабой ссылке
Возврат М_Базовый;
КонецФункции
М_КлючЭтого = СлабыеСсылки.ПолучитьКлюч(ЭтотОбъект); // создает слабую ссылку на себя
Итак, что здесь происходит. СпециальныйОбъект в момент создания экземпляра получает слабую ссылку на самого себя в виде УИДа и хранит ее все время своего существования. В своем конструкторе он создает экземпляр БазовыйОбъект и сохраняет сильную ссылку на него. Таким образом, существование экземпляра БазовыйОбъект гарантируется на все время жизни экземпляра СпециальныйОбъект.
В конструктор экземпляра БазовыйОбъект передается слабая ссылка на СпециальныйОбъект. Это не создает циклической зависимости. Точнее, циклическая зависимость временно существует, пока существует контейнер в кэше повторного использования, т.е. время жизни цикла ограничено временем жизни кэша.
Перед каждым вызовом базового объекта, СпециальныйОбъект повторно вызывает СлабыеСсылки.ПолучитьКлюч, чтобы "омолодить" кэш повторного использования. После этого контейнер предположительно должен жить еще 6 минут, что более чем достаточно для совершения вызова.
В свою очередь, БазовыйОбъект в самом начале своей функции получает сильную ссылку на СпециальныйОбъект, таким образом гарантируя, что экземпляр СпециальныйОбъект будет существовать в течение всего времени выполнения функции, даже если по какой-то причине все остальные ссылки на СпециальныйОбъект исчезнут.
Таким образом, технология слабых ссылок позволяет в ручном режиме разруливать циклические зависимости, даже в условиях отсутствия поддержки со стороны штатного сборщика мусора. И вполне реализуема средствами 1С.
А если бы 1С встроила такое решение в платформу, было бы еще лучше.
Вступайте в нашу телеграмм-группу Инфостарт
