Ну да ладно, что-то ударился в воспоминания: уже больше года на C++ не пишу (и рад бы, да не знаю куда его можно применить, не над теми задачами сейчас работаю). В Lisp это всё якобы не имеет большого значения, ибо есть сборка мусора. Нет, ну может оно так и есть для Lisp-машин, но я с ними не работал. А реальность состоит в том, что в основе значительной части современной IT-инфраструктуры лежит язык "C". А значит, сокеты, каналы, соединения с базой и т.п. сборщик мусора никак обработать не может. Но даже если бы мог, то вряд ли бы это улучшило ситуацию, часто эти ресурсы необходимо освобождать в четко определенные моменты и не ждать когда же запуститься сборка мусора (вот не закроешь канал и останется висеть дочерний процесс в памяти, и последствия мало предсказуемы: вдруг он ждет завершения ввода для начала выполнения основной работы).
Вывод из этого только один: сборка мусора может и хорошо, да только её мало, необходимо иметь более общий (и явно контролируемый) механизм для управления ресурсами.
"Lisp way" для решения этих проблем заключается в использовании макросов with-... Часто такой способ и удобен и эффективен. Но не всегда. Возьмем такой надуманный (и довольно глупый) пример:
(defun myprint (text stream)Что будет, если в качестве stream будет передан nil? Ну, текст будет выведен в *standard-output*. А если необходимо другое? Скажем, как нибудь так:
(write text :stream stream))
(defun myprint (text stream)Т.е. мы получили дублирование кода. Благо это пока только одна строка (write text :stream stream). А если там не одна строка? А много-много строк? Ну хорошо, можно сделать так:
(if stream
(write text :stream stream)
(with-open-file (out (print (sb-posix:mktemp "/tmp/XXXXXX")) :direction :output)
(write text :stream out))))
(defun myprint (text stream)Вроде всё хорошо? Ну а если в функцию передается несколько потоков и каждый из них может иметь значение nil и необходимо для каждого такого параметра создавать поток самостоятельно. Вот тут начинаются проблемы, ибо with-open-file нам больше не поможет (разве что только через совершенно жуткий код, пример которого я даже приводить боюсь). Нет, ну по настоящему мужественные люди возьмут в руки unwind-protect, что фактически эквивалентно переходу на ручное управление ресурсами, и без особых проблем напишут необходимый код. Только я так делать боюсь. Ибо это требует аккуратного кодирования, а славное прошлое C++-программиста научило меня избегать подобных вещей.
(flet ((myprint-impl (text stream)
(write text :stream stream)))
(if stream
(myprint-impl text stream)
(with-open-file (out (sb-posix:mktemp "/tmp/XXXXXX") :direction :output)
(myprint-impl text out)))))
Подобной ситуации с потоками у меня пока ещё не было (да и вряд ли будет, пример то надуман), но схожие ситуации при работе, например, с cffi встречают регулярно, и выглядит это примерно так:
(defun myfun (str1 str2 str3 str4)Всё отлично, вот только с-функция может принимать, как это часто бывает, в том числе NULL-ы, а значит в myfun можно передать несколько nil. Для передачи NULL в с-функцию необходимо использовать (cffi:null-pointer), но вот беда - cffi:with-foreign-strings ломается, если туда передавать nil. Приходиться брать в руки unwind-protect и заниматься ручным управлением памятью, т.е. почувствовать себя "многострадальным С-программистом", со всеми вытекающими.
(cffi:with-foreign-strings ((%str1 str1) (%str2 str2) (%str3 str3) (%str4 str4))
(c-фунция %str1 %str2 %str3 %str4)))
Однако, "многострадальные С-программисты" не так просты и их страдания малость преувеличены :-) В частности, разработчики Apache, для которых "ресурсы" святы (к чему приведут утечки ресурсов за несколько месяцев работы?) уже давно придумали эффективный и удобный механизм, который можно для краткости назвать APR Pools. Не вдаваясь в подробности, суть в том, что при выделении ресурса его необходимо немедленно зарегистрировать в пуле (их есть несколько, для запроса, для соединения и т.п.) и система автоматически освободит его (ресурс) при разрушении этого пула. Всё, имеющийся API делает проблему управления ресурсами тривиальной.
Надо же, С-хакеры оказались в этом куда более продвинуты :-) Ситуация является неприемлемой. Эта мысль привела меня к созданию garbage-pools: реализации подобия APR Pools на Common Lisp. С помощь этого несложного инструмента предыдущий пример можно написать так:
(defun myfun (str1 str2 str3 str4)garbage-pools имеет совсем небольшой API:
(flet ((foreign-string (str)
(if str
(gp:cleanup-register (cffi:foreign-string-alloc str)
#'cffi:foreign-string-free)
(cffi:null-pointer))))
(gp:with-garbare-pool ()
(с-функция (foreign-string str1)
(foreign-string str2)
(foreign-string str3)
(foreign-string str4)))))
- pool - класс пула ресурсов
- with-garbare-pool - макрос создающий и разрушающий пул. Возможно использование как именованных, так и безымянных пулов, например:
(with-garbare-pool ()
(cleanup-register myobj clenup-fun))(with-garbare-pool (mypool)
(cleanup-register myobj clenup-fun mypool)) - cleanup-register - регистрирует в пуле объект и "уничтожающую"-функцию
- cleanup-pool - вызывает разрушение всех зарегистрированных объектов
- cleanup-object - вызывает "преждевременное" разрушение зарегистрированного объекта
- object-register - "generic"-метод для регистрации объектов в пуле без указания функции "разрушителя". Это имеет смысл, когда функцию-разрушитель можно определить по типу объекта (из коробки поддерживаются только stream и pool)
- defcleanup - макрос, позволяющий сопоставить указанному классу функцию-разрушитель (после чего объекты данного классы можно будет регистрировать с помощью object-register). Пример:
(defcleanup stream #'close)
Комментариев нет:
Отправить комментарий