среда, 12 ноября 2008 г.

Управление ресурсами в Common Lisp

Вот за что люблю C++, так это за деструкторы (ну, конечно, не только за это). Последовательное следование принципу RAII привело к тому, что в течении последних нескольких лет активного использования C++ я не имел ни одной проблемы, связанной с утечкой ресурсов, даже когда писал с использование ObjectARX (Autocad), а это жуткий монстр, настоящее минное поле: шаг в сторону (ну в документации то про это ни слова) и все - лезут проблемы, но в каком-нибудь совершенно другом месте... Поэтому все байки о том, что при программировании на C++ есть какие-то большие проблемы с управлением ресурсами никогда всерьез не принимал: так было когда-то давно (ну когда компьютеры были большими, а программы маленькими), сейчас же достаточно просто последовательно использовать современным концепциям (RAII, интеллектуальные указатели, безопасная обработка исключений) и все эти рассказы о трехсуточных отладках можно смело отнести к области программистского фольклора былых дней.

Ну да ладно, что-то ударился в воспоминания: уже больше года на C++ не пишу (и рад бы, да не знаю куда его можно применить, не над теми задачами сейчас работаю). В Lisp это всё якобы не имеет большого значения, ибо есть сборка мусора. Нет, ну может оно так и есть для Lisp-машин, но я с ними не работал. А реальность состоит в том, что в основе значительной части современной IT-инфраструктуры лежит язык "C". А значит, сокеты, каналы, соединения с базой и т.п. сборщик мусора никак обработать не может. Но даже если бы мог, то вряд ли бы это улучшило ситуацию, часто эти ресурсы необходимо освобождать в четко определенные моменты и не ждать когда же запуститься сборка мусора (вот не закроешь канал и останется висеть дочерний процесс в памяти, и последствия мало предсказуемы: вдруг он ждет завершения ввода для начала выполнения основной работы).

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

"Lisp way" для решения этих проблем заключается в использовании макросов with-... Часто такой способ и удобен и эффективен. Но не всегда. Возьмем такой надуманный (и довольно глупый) пример:
(defun myprint (text stream)
(write text :stream stream))
Что будет, если в качестве stream будет передан nil? Ну, текст будет выведен в *standard-output*. А если необходимо другое? Скажем, как нибудь так:
(defun myprint (text stream)
(if stream
(write text :stream stream)
(with-open-file (out (print (sb-posix:mktemp "/tmp/XXXXXX")) :direction :output)
(write text :stream out))))
Т.е. мы получили дублирование кода. Благо это пока только одна строка (write text :stream stream). А если там не одна строка? А много-много строк? Ну хорошо, можно сделать так:
(defun myprint (text stream)
(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)))))
Вроде всё хорошо? Ну а если в функцию передается несколько потоков и каждый из них может иметь значение nil и необходимо для каждого такого параметра создавать поток самостоятельно. Вот тут начинаются проблемы, ибо with-open-file нам больше не поможет (разве что только через совершенно жуткий код, пример которого я даже приводить боюсь). Нет, ну по настоящему мужественные люди возьмут в руки unwind-protect, что фактически эквивалентно переходу на ручное управление ресурсами, и без особых проблем напишут необходимый код. Только я так делать боюсь. Ибо это требует аккуратного кодирования, а славное прошлое C++-программиста научило меня избегать подобных вещей.

Подобной ситуации с потоками у меня пока ещё не было (да и вряд ли будет, пример то надуман), но схожие ситуации при работе, например, с cffi встречают регулярно, и выглядит это примерно так:
(defun myfun (str1 str2 str3 str4)
(cffi:with-foreign-strings ((%str1 str1) (%str2 str2) (%str3 str3) (%str4 str4))
(c-фунция %str1 %str2 %str3 %str4)))
Всё отлично, вот только с-функция может принимать, как это часто бывает, в том числе NULL-ы, а значит в myfun можно передать несколько nil. Для передачи NULL в с-функцию необходимо использовать (cffi:null-pointer), но вот беда - cffi:with-foreign-strings ломается, если туда передавать nil. Приходиться брать в руки unwind-protect и заниматься ручным управлением памятью, т.е. почувствовать себя "многострадальным С-программистом", со всеми вытекающими.

Однако, "многострадальные С-программисты" не так просты и их страдания малость преувеличены :-) В частности, разработчики Apache, для которых "ресурсы" святы (к чему приведут утечки ресурсов за несколько месяцев работы?) уже давно придумали эффективный и удобный механизм, который можно для краткости назвать APR Pools. Не вдаваясь в подробности, суть в том, что при выделении ресурса его необходимо немедленно зарегистрировать в пуле (их есть несколько, для запроса, для соединения и т.п.) и система автоматически освободит его (ресурс) при разрушении этого пула. Всё, имеющийся API делает проблему управления ресурсами тривиальной.

Надо же, С-хакеры оказались в этом куда более продвинуты :-) Ситуация является неприемлемой. Эта мысль привела меня к созданию garbage-pools: реализации подобия APR Pools на Common Lisp. С помощь этого несложного инструмента предыдущий пример можно написать так:
(defun myfun (str1 str2 str3 str4)
(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)))))
garbage-pools имеет совсем небольшой API:
  • 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)


Комментариев нет:

Отправить комментарий