Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
kernigan_paik.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.91 Mб
Скачать

4.6. Управление ресурсами

Одна из наиболее серьезных проблем, требующих решения при про­ектировании интерфейса библиотеки (а также класса или пакета), — это управление ресурсами, которыми библиотека распоряжается самостоя­тельно или совместно с вызывающим ее окружением. Наиболее важным из таких ресурсов является память: кто должен ее выделять и высвобож­дать? Кроме того, среди других ресурсов есть открытые файлы, а также переменные, значения которых представляют общий интерес. Грубо го­воря, проблемы с ресурсами можно разделить на инициализацию, под­держание заданного состояния, совместное использование и копирова­ние, а также высвобождение.

В прототипе нашего пакета CSV для задания начальных значений указателей, счетчиков и прочих подобных вещей применялась статиче­ская инициализация. Однако подобный подход довольно ограничен: мы не можем вернуть библиотеку в начальное состояние после того, как были вызваны какие-либо функции этой библиотеки. Альтернативный способ инициализации — создание отдельной специальной функции, которая бы устанавливала все внутренние переменные в корректные начальные значения. При таком подходе возврат в стартовое состояние возможен в любой момент, даже после вызова функций библиотеки, однако пользователь должен будет сам вызывать эту функцию явным образом. Для этой цели функция reset из второй версии библиотеки могла бы быть сделана видимой (то есть public).

В C++ и Java для инициализации данных внутри класса используют­ся конструкторы. Должным образом определенные конструкторы дают нам гарантию, что все данные класса инициализированы и способа со­здать неинициализированный объект не существует. Набор конструк­торов может поддерживать различные виды инициализации. Так, мы могли бы снабдить Csv конструктором, получающим имя файла, или конструктором, получающим входной поток.

А как насчет копирования информации, обрабатываемой библио­текой, — такой, как вводимые строки и поля? Наша С-программа csvgetline предоставляет прямой доступ к вводимым данным (строкам и полям), возвращая указатели на них. У такого свободного доступа существует ряд недостатков. Пользователь может перезаписать па­мять, так что информация окажется некорректной. Например, выра­жение вроде

strcpy(csvfield(1), csvfield(2));

может в целом ряде случаев сработать некорректно, — скорее всего, пе­резаписав начало второго поля, если оно окажется длиннее первого. Пользователь библиотеки должен сделать копию всей информации, ко­торую нужно будет сохранить после очередного вызова csvgetline. Так, после выполнения вот такого фрагмента кода, указатель вполне может оказаться неверным, если второй вызов csvgetline приведет к новому выделению памяти для буфера строк:

char *p;

csvgetline(fin);

р = csvfield(1);

csvgetline(fin);

/* здесь р может оказаться неверным */

Версия на C++ безопаснее, поскольку строки в ней являются всего лишь копиями, которые можно менять как заблагорассудится.

Java использует ссылки для обращения к объектам, то есть ко всему, кроме базовых типов вроде int. Это более эффективно, чем создание ко­пий, однако пользователь может быть введен в заблуждение, считая, что ссылка является копией; ошибка подобного рода имела место в ранней Java-версии программы markov. Надо сказать, что данная проблема яв­ляется вечным источником ошибок при работе со строками С. Не стоит забывать, что при необходимости создания копии методы клонирования (позволяют вам сделать и это.

Обратной стороной инициализации или конструирования чего-либо является его финализация (fmalization), или деструкция, — то есть очист­ка и высвобождение ресурсов после того, как они больше не нужны. Особенно важно высвобождение памяти. Очевидно, что программе, которая не высвобождает неиспользуемую память, этой самой памяти в какой-то момент не хватит. Как ни странно, большая часть современных программ страдает этим недостатком. Схожая проблема возникает и в ситуации, согда приходит время закрывать открытые файлы: если данные были бу­феризованы, этот буфер нередко надо уничтожить (а память, занимаемую им, очистить). Для функций стандартной библиотеки С высвобождение происходит автоматически после нормального окончания работы про­граммы, все остальные случаи должны обрабатываться программой. В С C++ стандартная функция atexit предоставляет способ получить управ­ление непосредственно перед тем, как программа будет завершена нор­мально; создателям интерфейсов не стоит пренебрегать такой возмож­ностью для высвобождения ресурсов.

Высвобождайте ресурсы на том же уровне, где выделяли их. Хороший способ управления выделением и высвобождением ресурсов —возло­жить ответственность за освобождение ресурса на ту же библиотеку, па­кет или интерфейс, которые выделяют этот ресурс. Можно выразить эту мысль и другими словами: состояние ресурса не должно меняться в пре­делах интерфейса. Все функции наших библиотек CSV считывали дан­ные из уже открытых файлов, и по окончании работы они оставляли файлы открытыми. Закрытием файлов должны были заниматься те, кто Ьнх открывал, то есть пользователи библиотеки.

Конструкторы и деструкторы C++ помогают строго выполнять это правило. Когда экземпляр класса выходит из области видимости или явным образом уничтожается, вызывается деструктор. В этом деструк­торе можно уничтожать буферы, освобождать память, возвращать зна­чения в исходное состояние и делать вообще все, что необходимо. В Java подобного механизма нет. Можно определить для класса метод финали-зации, однако нельзя быть уверенными, что он будет выполнен вообще, не говоря уже о том, чтобы выполниться в какое-то конкретное время. Таким образом, нельзя дать гарантий, что действия по высвобождению ресурсов будут выполнены, хотя зачастую можно предполагать, что это все же произойдет.

В Java, однако, существует механизм, оказывающий огромную по­мощь в управлении ресурсами, — встроенная сборка мусора (garbage collection). При запуске программы выделяется память под новые объекты. Способа удалить их явным образом просто нет, однако некая система времени исполнения отслеживает, какие объекты все еще ис­пользуются, а какие нет, и периодически удаляет неиспользуемые.

Существуют различные способы реализации сборки мусора. В некоторых схемах отслеживается счетчик ссылок (reference count) — некоторое число, показывающее, сколькими объектами используется интересующий нас объект. Объект высвобождается, как только счетчик ссылок становится рав­ным нулю. Эту технологию можно реализовать явным образом в С и C++ для управления совместно используемыми объектами. Другой алгоритм пе­риодически ищет связи между выделенной областью памяти и всеми объек­тами, на которые имеются ссылки. Объекты, обнаруживаемые при этом, кем-то используются, объекты же, на которые никто не ссылается, соответ­ственно, не используются и могут быть уничтожены.

Наличие автоматической сборки мусора не означает, что при проек­тировании можно оставить вопросы управления ресурсами без внима­ния. Нам все равно надо определить, возвращает ли интерфейс ссылки на совместно используемые объекты или их копии, а это оказывает боль­шое влияние на всю программу. И вообще, бесплатной сборки мусора не бывает, за нее приходится платить дополнительными расходами на под­держание информации и высвобождение неиспользуемой памяти; кро­ме того, невозможно предсказать моменты, когда эта сборка мусора за­работает.

Все описанные проблемы становятся еще более запутанными, если библиотека должна использоваться в среде, где ее функции могут испол­няться одновременно в нескольких нитях управления — как, например, в многонитевой программе на Java.

Чтобы избежать лишних проблем, необходимо писать реентерабельный (reentrant, повторно вызываемый) код, то есть код, который бы работал вне зависимости от количества одновременных его вызовов. В реентерабель­ном коде не должно быть глобальных переменных, статических локаль­ных переменных, а также любых других переменных, которые могут быть изменены в то время, как их использует другая нить. Основой хорошего проекта многонитевой программы является такое разделение компонен­тов, при котором они не могут ничего использовать совместно иначе, чем через должным образом описанный интерфейс. Библиотеки, в которых по небрежности переменные доступны для совместного использования, спо­собны разрушить многонитевую модель. (В многонитевой программе ис­пользование strtok может привести к ужасным последствиям, поскольку существуют другие функции из библиотеки С, которые хранят значения во внутренней статической памяти.) Если переменная может быть исполь­зована несколькими процессами, то необходимо предусмотреть некий блокирующий механизм, который бы давал гарантию, что в любой момент времени с ними может работать только одна нить. Здесь очень полезны классы, поскольку они создают основу для обсуждения моделей совместного использования и блокировки. Синхронизированные методы в Java к предоставляют нити управления способ заблокировать целый класс или его экземпляр от одновременного изменения другой нитью; синхронизированные блоки разрешают только одной нити за раз выполнять фрагмент кода.

Многонитевое управление добавляет немало новых сложностей во многие аспекты проектирования и программирования; тема эта чересчур обширна, чтобы обсуждать ее в деталях на страницах этой книги.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]