Автор: Anton Ertl
Статья опубликована на
ЕвроФорт2008
Перевел:
mOleg
Чистим за собой.
Аннотация.
Выполнение действий очистки, таких как восстановление переменных или закрытие файлов невозможно было гарантировать в Форте до 94 стандарта, который дал нам CATCH THROW механизм. Но даже с CATCH возможны ситуации, когда восстановление будет пропущено из за пользовательских прерываний, если вы будете неудачливы. Мы предлагаем конструкцию, которая гарантирует, что код
восстановления(очистки) всегда будет выполнен. Мы так же обсудим более легкие предложения реализации для кода чистки восстановления), нежели использование полного фрейма исключения.
Введение
Частая проблема, встречаемая в программировании - восстановление некоторого состояния, освобождения ресурса, или выполнения другого обязательного завершающего действия. Типичные примеры:
- восстановление переменной BASE после временного изменения;
- закрытие файла.
В Форт-94 мы можем использовать CATCH для гарантии что такая очистка произойдет в большинсте (но не во всех) случаях. В этой статье мы исследуем пути для улучшения состояния дел следующим образом:
- предоставим более надежный механизм, который работает даже в случае асинхронного исключения (в том числе, пользовательского прерывания);
- избегание полного фрейма исключения, где это возможно.
Используемый пример.
Как используемый пример, мы будем использовать слово hex. , которое печатает число в шестнадцатиричной системе исчисления без изменения состояния переменной BASE . И это слово будет использоваться в следующем контексте:
: foo
... hex. ...
... . ... ;
DECIMAL foo
HEX foo
Заметте, что в дополнение к печатанию числа в шестнадцатиричной системе , foo так же печатает числов в текущей системе исчисления.
Классическое форт-решение.
Подход из "Thinking Forth".
В старые дни, когда форт не имел механизма CATCH THROW , было бы написано подобным образом:
: hex. ( u --> )
BASE @ >R
HEX U.
R> BASE ! ;
Но даже тогда были возможны не локальные выходы через ABORT и QUIT , так же как и через пользовательские прерывания. Если любые из этих нелокальных выходов или прерываний произойдут, BASE не будет восстановлена.
Таким образом, раньше восстановление не могло быть произведено гарантированно. В результате были изобретены различные пути для обхождения проблемы, как, например, в "Thinking Forth", параграф 7, секция сохранение и восстановление состояний[1]. В особенности, эта секция ссылается на Чака Мура следующим образом:
"Вы действительно завязаны в узел. Вы создаете проблему для себя. Если я хочу получить шестнадцатиричный дамп, я говорю: HEX DUMP . Если я хочу получить десятичный дамп, я говорю: DECIMAL DUMP . Я не позволяю слову DUMP изменять мое окружение.
Существует философский выбор между восстановлением состоянии по окончании действия, и предопределением состояния в начале действия. Длительное время я думал, что лучше восстанавливать состояние по окончании действия. И я пытался так поступать везде, но очень сложно так поступать всегда. Теперь я стараюсь предопределять состояние перед началом действия.
Чем заботиться о переменных, лучше устанавливать их. Если кто-то другой изменяет переменные, он не должен заботиться о их восстановлении. Всегда больше выходов, чем входов."
К сожалению, этот совет не подходит к нашему выбранному примеру: как может быть разрешена ситуация перед вызовом слова . в foo ? И этот совет не поможет нам во многих других задачах подчистки подобных закрытию файлов и освобождению других ресурсов.
Использование CATCH.
К счастью, с появлением механизма CATCH в 94 стандарте ситуация изменилась: существует всего один выход из CATCH , и мы можем использовать это свойство для освобождения ресурсов более надежно:
: hex.-helper ( u -- ) hex u . ;
: hex. ( u --> ) BASE @ >R ['] hex.-helper CATCH R> base ! THROW
Это позволяет быть уверенным, что переменная будет восстановлена даже в случае возникновения исключения (любого типа) во время исполнения слова hex.-helper .
Тем не менее существует одна прореха в нашей броне: Если исключение ( в том числе пользовательское прерывание) произойдет во время восстановления BASE, восстановительный код не будет завершон, и переменная сохранит неверное состояние.
Улучшенные решения.
Try...restore...endtry
В текущей верси GFORTH предлагается конструкция try code1 restore code2 endtry . Если любое исключение возникнет где-нибудь между try и endtry (в том числе в code2 ), глубина стека данных сбросится на состояние, предшествующее выполнению try , значение исключения будет положено на стек данных, и исполнение будет передано коду начинающегося со слова restore .
С этой конструкцией имеем не только один выход, так же гарантируется, что code2 будет исполнено с начала до конца.
Эта конструкция может использоваться для решения нашей проблемы следующим образом:
Код:
: hex. ( u -- )
BASE @ { oldbase }
try
hex .
0 \ значение для THROW
restore
oldbase BASE !
endtry
THROW ;
Старое значение переменной BASE сохраняется в локальной переменной, потому что мы не можем использовать стек возвратов для этой цели (try кладет фрейм исключения на стек возвратов). Однако, в замен использования локальной переменной, мы можем использовать стек данных следующим образом:
Код:
: hex. ( u -- )
base @
try
over hex .
0 \ значение для THROW
restore
over base !
endtry
THROW
2DROP ;
Пример того, как можно использовать OVER для хранения значений на стеке данных нетронутым так, что мы можем использовать их в восстанавливающем коде. Только мы должны эти значение удалить после выполнения кода endtry.
Данная конструкция требует некоторой осторожности при использвании:
- как показано выше, необходимо заботиться о том, чтобы значение с вершины стека данных не снималось; оно не должно сниматьс оттуда даже во время восстановление состояния.
- восстанавливающий код не должнен вызывать исключений как минимум во время своего исполнения. Иначе система попадет в бесконечный цикл состоящий из начала восстановительного процесса и вызова нового исключения. И пользователь не сможет прервать данный процесс не используя некорректные методы ( такие как сброс, или посылка сигнала SIGTERM в юниксе).
- Восстанавливающий код должен иметь возможность многократного исполнения, то есть не должен изменять состояние стека данных во время своего исполнения, и повторное выполнение кода должно приводить к такому же результату, как и однократное.
В любом случае, форт-программисты относятся ответсвтенно к своим программам и эти предупреждения не должны быть проблемой. Предъявляемые требования не всегда возможно реализовать (либо очень сложно), когда очистка\восстановление относится к закрытию файлов или освобождения блоков памяти. В таких случаях обычно предпочтительно иметь небольшой шанс невыполнения очистки, чем
многократного выполнения. Некоторые могут пытаться разрешить проблему с помощью размещения кода, который не должен быть повторно выполнен между словами endtry и THROW .
В случаях, когда переменные изменяются и восстанавливаются такое требование выполнить легко. Пример последнего:
Код:
... open-file throw { f }
try
... f read-file throw ...
0 restore
endtry
f close-file throw
throw
Слова специального назначения.GForth так же имеет слова специального назначение для редких, либо опасных назначений:
base-execute ( i*x xt u -- j*x ) - выполняет xt пока BASE установлена в u.
infile-execute ( i*x xt file-id -- j*x ) - выполняет xt пока KEY итп читает входной поток из file-id
outfile-execute ( i*x xt file-id -- j*x ) - выполняет xt пока исходящий поток, TYPE и подобные перенаправлены в file-id.
Задано, что все эти слова берут исполнимый адрес со стека, и этот адрес почти всегда константа, возможно лучше создавать новые определения подобно представленным, чтобы они могли брать исполнимый адрес с вершины стека данных.
Эффективность. Фрейм исключения занимает пять ячеек на стеке возвратов в Gforth (а так же, вероятно, в других системах), а так же отнимает некоторое время на его оформление. Для вопросов восстановления полный фрейм исключения избыточен. Мы не нуждаемся в восстановлении всех стеков в данном случае: если мы вошли в востанавливающую часть нормальным образом, стеки не /восстанавливаются вообще; если мы попали в восстанавливающую часть с помощью THROW, мы передаем исключение дальше, и нам не надо восстанавливать стеки. То есть нам нужна только информация, достаточная для восстановления данных.
Таким образом мы можем реализовать легковесный механизм для восстановления. Двух ячеек для фрейма будет достаточно. Восстановительные фреймы могут храниться на стеке возвратов, и быть связанными в однонаправленный список. THROW будет исполнять все восстанавливающие фреймы, находящиеся перед следующим фреймом исключения на стеке возвратов, затем
обрабатывать фрейм исключения обычным образом.
Обратная сторона такого подхода заключается в том, что восстанавливающий код и данные могут быть более осведомлены о механизме восстановления, потому что восстанавливаемые данные не могут быть прямо переданы через стеки, но могут быть доступны через фрейм восстановления. То есть восстанавливающий код для BASE может выглядеть следующим образом:
Код:
: restore-base ( addr -- )
dup @ base !
next-restoration ;
Тут, addr - это адрес пользовательской части фрейма восстановления. Next-restoration ( addr -- ) удаляет текущий фрейм восстановления из цепочки. Любая последовательность, которую нельзя исполнять повторно должна происходить после next-restoration .
Реализация base-execute с подобным механизмом может выглядеть так:
Код:
: base-execute ( i*x xt u -- j*x )
base @ >r
['] restore-base >restore
base ! execute
restore>
r> drop ;
Где, >restore будет добавлять восстановительный фрейм на стек возвратов и связвать в восстановительную цепочку. Restore> будет исполнять восстанавливающий xt и удалять его с вершины стека возвратов. Старая BASE будет полностью удалена.
Предлагаемый механизм еще не реализован. Пока его будет сравнительно легко создать, еще не ясно, стоит ли сопровождать документацией и поддержкой для предоставления пользователям. Имеется несколько моментов для обсуждения:
- мой опыт показывает, что почти все пользователи пользуются CATCH механизмом для восстановления\очистки. Таким образом большинство фреймов исключения может быть заменено более легкими восстанавливающими фреймами.
- фреймы исключений и их поддержка не показали критического понижения быстродействия, но я не производил никаких измерений.
Родственная работа.
Я не осведомлен о других расширенных решениях в форте. Ведь это часто встречающаяся программистская проблема, поэтому в других языках разработана широкий выбор решений.
Динамический контекст.
Часть проблем, рассмотренных в данной статье, например, наш вариант работы с BASE может быть рассмотрен, как подгонка окружения исполнения. Хансон и Проебстринг [2] аргументирует, что переменные динамического контекста имеют правильные свойства для этого использования и программистам языков без поддержки динамического окружения приходится симулировать динамическое окружение. Они указывают на подобность между исключениями (система с динамическим окружением) и динамически изменяемых переменных (которые объясняют почему обычно используют ловлю исключений в реализациях).
Значительное число языков программирования и систем предоставляют переменные динамического контекста. Но, вероятно, более широко используемый пример - переменные окружения в UNIX и WINDOWS процессах.
Другой известный язык с переменными динамического контекста - Postscript, в котором программисты выполняют динамический контекст с помощью (с использованием терминологии Форта) создавая словари динамически, и добавляя и в стек контекста, потому что поиск имет в Постскрипте производится во время исполнения кода, это и создает динамический контекст. В любом случае слова
динамического управления (exit, stop) не воздействуют на глубину стека передачи управления (возвратов), и обсуждаемые свойтсва не могут быть скомбинированы безопастным образом.
Очистка.
Лисп имеет специальный механизм unwind - protect обеспечивающий уверенность в том, что очистка будет выполнена в любом случае, даже при критическом выходе из protected. Однако, в отличие от try...restore...endtry механизм не защищает от исключения из cleanup части. Ява имеет подобный механизм в форме try...finally конструкции, и в С++ в виде try...catch . С++ так же предоставляет механизм деструкторов, которые могут автоматически освобождать ресурсы и выполнять другую освобождающую работу когда контекст переменной удален. Страуструп [3] дает хороший обзор подобного рода безопастных исключений, поэтому теперь многие решения на C++ их используют. Аналогично финализаторы в Яве выполняют освобождающие действия во время сборки мусора. Так как финализатор может быть исполнен значительно позже деструктора, часто рекомендуют предпочитать другие подходы вместо финализаторов. Много других языков имеет подобные средства.
Итог.
Введение механизма CATCH в 94 стандарте языка Форт предоставляет хороши базис для написания кода, который чистит за собой, и не требует кода чистящего мусор впоследствии. Тем не менее, существование пользовательских прерываний и асинхронных исключений делает существующие механизмы недостаточными. В статье предлагается try ... restore ... endtry конструкция для решения перечисленных проблем полность, но не во всех случаях. Были так же рассмотрены более легкие реализации механизма.
Ссылки.
[1] Leo Brodie. Thinking Forth. Fig Leaf
Press (Forth Interest Group), 100 Dolores
St, Suite 183, Carmel, CA 93923, USA,
1984.
[2] David. R. Hanson and Todd A. Proebsting.
Dynamic variables. In SIGPLAN '01 Conference
on Programming Language Design
and Implementation, pages 264-273, 2001.
[2] Bjarne Stroustrup. Exception safety: concepts
and techniques. In Advances in exception
handling techniques, pages 60-76.
Springer LNCS 2022, 2001.