Управление ресурсами
Высвобождайте ресурсы на том же уровне, где выделяли их.
Хороший способ управления выделением и высвобождением ресурсов – возложить ответственность за освобождение ресурса на ту же библиотеку, пакет или интерфейс, которые выделяют этот ресурс. Можно выразить эту мысль и другими словами: состояние ресурса не должно меняться в пределах интерфейса. Все функции наших библиотек CSV считывали данные из уже открытых файлов, и по окончании работы они оставляли файлы открытыми. Закрытием файлов должны были заниматься те, кто их открывал, то есть пользователи библиотеки.
Конструкторы и деструкторы C++ помогают строго выполнять это правило. Когда экземпляр класса выходит из области видимости или явным образом уничтожается, вызывается деструктор. В этом деструкторе можно уничтожать буферы, освобождать память, возвращать значения в исходное состояние и делать вообще все, что необходимо. В Java подобного механизма нет. Можно определить для класса метод финализации, однако нельзя быть уверенными, что он будет выполнен вообще, не говоря уже о том, чтобы выполниться в какое-то конкретное время. Таким образом, нельзя дать гарантий, что действия по высвобождению ресурсов будут выполнены, хотя зачастую можно предполагать, что это все же произойдет.
В Java, однако, существует механизм, оказывающий огромную помощь в управлении ресурсами, – встроенная сборка мусора (garbage collection). При запуске программы выделяется память под новые объекты. Способа удалить их явным образом просто нет, однако некая система времени исполнения отслеживает, какие объекты все еще используются, а какие нет, и периодически удаляет неиспользуемые.
Существуют различные способы реализации сборки мусора. В некоторых схемах отслеживается счетчик ссылок (reference count) – некоторое число, показывающее, сколькими объектами используется интересующий нас объект. Объект высвобождается, как только счетчик ссылок становится равным нулю. Эту технологию можно реализовать явным образом в С и C++ для управления совместно используемыми объектами. Другой алгоритм периодически ищет связи между выделенной областью памяти и всеми объектами, на которые имеются ссылки. Объекты, обнаруживаемые при этом, кем-то используются, объекты же, на которые никто не ссылается, соответственно, не используются и могут быть уничтожены.
Наличие автоматической сборки мусора не означает, что при проектировании можно оставить вопросы управления ресурсами без внимания. Нам все равно надо определить, возвращает ли интерфейс ссылки на совместно используемые объекты или их копии, а это оказывает большое влияние на всю программу. И вообще, бесплатной сборки мусора не бывает, за нее приходится платить дополнительными расходами на поддержание информации и высвобождение неиспользуемой памяти; кроме того, невозможно предсказать моменты, когда эта сборка мусора заработает.
Все описанные проблемы становятся еще более запутанными, если библиотека должна использоваться в среде, где ее функции могут исполняться одновременно в нескольких нитях управления – как, например, в многонитевой программе на Java.
Чтобы избежать лишних проблем, необходимо писать реентерабельный (reentrant, повторно вызываемый) код, то есть код, который бы работал вне зависимости от количества одновременных его вызовов. В реентерабельном коде не должно быть глобальных переменных, статических локальных переменных, а также любых других переменных, которые могут быть изменены в то время, как их использует другая нить. Основой хорошего проекта многонитевой программы является такое разделение компонентов, при котором они не могут ничего использовать совместно иначе, чем через должным образом описанный интерфейс. Библиотеки, в которых по небрежности переменные доступны для совместного использования, способны разрушить многонитевую модель. (В многонитевой программе использование strtok может привести к ужасным последствиям, поскольку существуют другие функции из библиотеки С, которые хранят значения во внутренней статической памяти.)
Если переменная может быть использована несколькими процессами, то необходимо предусмотреть некий блокирующий механизм, который бы давал гарантию, что в любой момент времени с ними может работать только одна нить. Здесь очень полезны классы, поскольку они создают основу для обсуждения моделей совместного использования и блокировки. Синхронизированные методы в Java предоставляют нити управления способ заблокировать целый класс или его экземпляр от одновременного изменения другой нитью; синхронизированные блоки разрешают только одной нити за раз выполнять фрагмент кода.
Многонитевое управление добавляет немало новых сложностей во многие аспекты проектирования и программирования; тема эта чересчур обширна, чтобы обсуждать ее в деталях на страницах этой книги.