Освобождение ресурсов
Освобождение (и управление вообще) ресурсов — стандартная задача в прикладном программировании, поскольку практически любая программа взаимодействует с различными ресурсами операционной системы или иными объектами.
Ресурс
Это объект в программе, представляющий некоторый ресурс, будь операционной системы или вычислительной системы. Такой объект имеет определенный цикл жизни: он получается, его используют, а после обязательно освобождают.
Например, в качестве ресурса можно представить файловый дескриптор, сетевое соединение, поток или даже буфер памяти, необходимый в строгом контроле.
Неосвобожденный ресурс может привести к нарушениям работы программы, так как ресурсы подразумевают некоторую их ограниченность, ценность. Например, открытие множества файловых дескрипторов может привести к тому, что у операционной системы закончится их количество для процесса, после чего без должной обработки это может привести к завершению, либо нарушенной работы.
Как это в других языках?
В языке вроде C++ или Rust для это существует нативная поддержка механизма RAII, но он не сильно подходит средам с автоматическим управлением памятью (т.е GC).
Далее приведены идиомы и возможные варианты как это делать правильно на OCaml.
Видео-иллюстрация из серии OCaml Tips
Смотреть на Youtube.
Ручное закрытие
👂 Не рекомендуем к использованию, когда того не требует низкоуровенные подробности!
При традиционном (ручном) освобождении ресурсов (например, с помощью функции close) могут возникнуть множество проблем — это можно забыть сделать или закрыть слишком рано, либо вовсе сделать неправильно
Пример ручного закрытия файла
let read_first_line_from_file filename =
let file = open_in filename in
let line = input_line file in
close_in file;
lineПреимущества и недостатки
- 🚩 Требует ручного контроля: легко забыть, многословно;
- 🚩 Явная обработка исключений для корректного закрытия ресурса;
- 🚩 Громоздко при множестве открытых ресурсов;
Функция открытия — также функция закрытия
Функция открытия — также функция закрытия, или же with_ функция, распестренный паттерн как в стандартной библиотеке, так и в сторонних.
let _ =
File.with_open filename @@ fun resource ->
(* processing *)
(* auto close file *)Суть паттерна заключается в том, чтобы в открывающую ресурс функцию передавать сразу callback, который как-бы будет исполняться внутри контекста открытого файла. При этом в случае исключения, ресурс будет корректно закрыт.
Смотрите также
Пример реализации with_ функции
Используйте функцию Fun.protect, вам не следует вручную обрабатывать возникающие исключения! Так как в библиотечной функции уже правильно реализована передача stack trace'а исключения.
let initialize _ = (* ... *)
let dispose _ = (* ... *)
let with_initialize _ f =
let resource = initialize _ in
Fun.protect
(fun () -> f resource)
~finally:(fun () -> dispose resource)Преимущества и недостатки
- 🚩 Образует вложенность при множестве ресурсов в одной области видимости;
- 🚩 Не поддается контролю, когда именно освобождать ресурс;
Switches
В других языках
Схожую концевую на уровне языка можно найти, например, в Python и C#. В первом это называется context manager. В C# же она называется try-with-resources.
В обоих случаях они опираются на свою объектную систему и на те свойства, что их объекты-ресурсы реализуют disposable интерфейс.
Switch (он же свитч) — область видимости, технически работающая по принципу наличия hooks (aka callbacks), привязанных к ней, и вызываемых по закрытию этой области.
Таким образом для пользователя мы как-бы привязываем ресурс к свитчу, но технически привязываем hook по его освобождению, что существенно гибче, чем предыдущие подходы.
Пример использования Eio.Switch
Этот паттерн можно особенно ярко встретить в библиотеке Eio. Он там повсеместен и без него нельзя ничего сделать, так как ресурс должен быть к чему-то привязан.
let run_client ~net ~addr =
Switch.run ~name:"client" @@ fun sw ->
traceln "Client: connecting to server";
let flow = Eio.Net.connect ~sw net addr in
(* Read all data until end-of-stream (shutdown): *)
traceln "Client: received %S" (Eio.Flow.read_all flow)Пример использования Lwt_switch
В Lwt тоже можно найти свитчи как доп. абстракции — модуль Lwt_switch.
let main =
Lwt_switch.with_switch @@ fun sw ->
let* file = Lwt_io.open_file "some-file" ~mode:Output in
Lwt_switch.add_hook (Some sw) (fun () -> Lwt_io.close file);
Lwt.return_unitЧитайте подробнее на основной странице.
Преимущества и недостатки
- 🚩 Требует наличия реализации Switch'а и поддержки со стороны вызываемых функций;
Освобождение при уничтожении объекта
👂 Не используйте это!
Не поддается контролю и полностью полагается на детали реализации. Это действительно не то, что вы хотите. Эта секция приведена исключительно в образовательных целях.
Используя интерфейс сборщика мусора (модуль Gc) мы можем установить функцию, что должна быть выполнена во время освобождения объекта сборщика мусора.
Работает исключительно на heap-allocated объектах!
Стоит понимать
Освобождения ресурса произойдёт только тогда, когда OCaml решит очистить объект, а это может не произойти вообще. Смотрите Memory management.
Библиотека Lwt
В пакете lwt.unix (часть Lwt) есть модуль Lwt_gc и функция finalise_or_exit, которая гарантирует, что при завершении программы ресурс будет освобождён (будет вызвана функция, в которой произойдёт освобождение).
Пример
let main =
let res = String.make 10 'x' in
Lwt_gc.finalise_or_exit (Lwt_io.printlf "free '%s'") res;
Lwt.return_unit