Skip to content

Освобождение ресурсов

Освобождение (и управление вообще) ресурсов — стандартная задача в прикладном программировании, поскольку практически любая программа взаимодействует с различными ресурсами операционной системы или иными объектами.

Ресурс

Это объект в программе, представляющий некоторый ресурс, будь операционной системы или вычислительной системы. Такой объект имеет определенный цикл жизни: он получается, его используют, а после обязательно освобождают.

Например, в качестве ресурса можно представить файловый дескриптор, сетевое соединение, поток или даже буфер памяти, необходимый в строгом контроле.

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

Как это в других языках?

В языке вроде C++ или Rust для это существует нативная поддержка механизма RAII, но он не сильно подходит средам с автоматическим управлением памятью (т.е GC).

Далее приведены идиомы и возможные варианты как это делать правильно на OCaml.

Видео-иллюстрация из серии OCaml Tips

Смотреть на Youtube.

Ручное закрытие

👂 Не рекомендуем к использованию, когда того не требует низкоуровенные подробности!

При традиционном (ручном) освобождении ресурсов (например, с помощью функции close) могут возникнуть множество проблем — это можно забыть сделать или закрыть слишком рано, либо вовсе сделать неправильно

Пример ручного закрытия файла

ocaml
let read_first_line_from_file filename = 
  let file = open_in filename in 
  let line = input_line file in 
  close_in file;
  line
Преимущества и недостатки
  • 🚩 Требует ручного контроля: легко забыть, многословно;
  • 🚩 Явная обработка исключений для корректного закрытия ресурса;
  • 🚩 Громоздко при множестве открытых ресурсов;

Функция открытия — также функция закрытия

Функция открытия — также функция закрытия, или же with_ функция, распестренный паттерн как в стандартной библиотеке, так и в сторонних.

ocaml
let _ = 
  File.with_open filename @@ fun resource ->
    (* processing *)
  (* auto close file *)

Суть паттерна заключается в том, чтобы в открывающую ресурс функцию передавать сразу callback, который как-бы будет исполняться внутри контекста открытого файла. При этом в случае исключения, ресурс будет корректно закрыт.

Пример реализации with_ функции

Используйте функцию Fun.protect, вам не следует вручную обрабатывать возникающие исключения! Так как в библиотечной функции уже правильно реализована передача stack trace'а исключения.

ocaml
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. Он там повсеместен и без него нельзя ничего сделать, так как ресурс должен быть к чему-то привязан.

ocaml
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.

ocaml
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 объектах!

Материалы

Для этого смотрите функции finalise и alarm.

Стоит понимать

Освобождения ресурса произойдёт только тогда, когда OCaml решит очистить объект, а это может не произойти вообще. Смотрите Memory management.

Библиотека Lwt

В пакете lwt.unix (часть Lwt) есть модуль Lwt_gc и функция finalise_or_exit, которая гарантирует, что при завершении программы ресурс будет освобождён (будет вызвана функция, в которой произойдёт освобождение).

Пример
ocaml
let main = 
  let res = String.make 10 'x' in
  Lwt_gc.finalise_or_exit (Lwt_io.printlf "free '%s'") res;
  Lwt.return_unit