Как остановить сборку мусора OCaml моим обработчиком реактивных событий?

Я пытаюсь использовать библиотеку OBus с Lwt_react. Это использует "функциональное реактивное программирование" для свойств и сигналов.

Проблема (как отмечено в документации React) состоит в том, что OCaml может собирать ваш обратный вызов, пока вы все еще используете его. Есть keep функция, которая держит обработчик навсегда, но я этого не хочу. Я хочу освободить его в конце концов, но не тогда, когда мне все еще это нужно.

Итак, я решил присоединить обработчик к переключателю:

let keep ~switch handler =
  Lwt_switch.add_hook (Some switch) (fun () ->
    ignore handler;
    Lwt.return ()
  )

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

Вот упрощенная (автономная) версия моего кода:

(* ocamlfind ocamlopt -package react,lwt,lwt.react,lwt.unix -linkpkg -o test test.ml *)

let finished_event, fire_finished = React.E.create ()

let setup () =
  let switch = Lwt_switch.create () in

  let finished, waker = Lwt.wait () in
  let handler () = Lwt.wakeup waker () in
  let dont_gc_me = Lwt_react.E.map handler finished_event in
  ignore dont_gc_me;  (* What goes here? *)

  print_endline "Waiting for signal...";
  Lwt.bind finished (fun () -> Lwt_switch.turn_off switch)

let () =
  let finished = Lwt.protected (setup ()) in

  Gc.full_major ();  (* Force GC, to demonstrate problem *)
  fire_finished ();  (* Simulate send *)

  Lwt_main.run finished;
  print_endline "Done";

Без Gc.full_major линия, это обычно печатает Done, С этим он просто висит на Waiting for signal...,

Изменить: я разделил setup (реальный код) из тестового драйвера и добавил Lwt.protected обертка, чтобы избежать маскировки проблемы случайно отмены Lwt.

4 ответа

Решение

Вот фрагмент из моего проекта, исправленный для решения этой проблемы слабых ссылок (спасибо!). Первая часть - сохранить глобальный корень, указывающий на ваш объект. Вторая часть состоит в том, чтобы ограничить жизнеспособность сигнала / события в пределах потока Lwt.

Обратите внимание, что реактивный объект клонирован и явно остановлен, что может не совсем соответствовать вашим ожиданиям.

module Keep : sig 
  type t
  val this : 'a -> t
  val release : t -> unit
end = struct
  type t = {mutable prev: t; mutable next: t; mutable keep: (unit -> unit)}
  let rec root = {next = root; prev = root; keep = ignore}

  let release item =
    item.next.prev <- item.prev;
    item.prev.next <- item.next;
    item.prev <- item;
    item.next <- item;
    (* In case user-code keep a reference to item *)
    item.keep <- ignore

  let attach keep =
    let item = {next = root.next; prev = root; keep} in
    root.next.prev <- item;
    root.next <- item;
    item

  let this a = attach (fun () -> ignore a)
end

module React_utils : sig
  val with_signal : 'a signal -> ('a signal -> 'b Lwt.t) -> 'b Lwt.t
  val with_event  : 'a event -> ('a event -> 'b Lwt.t) -> 'b Lwt.t
end = struct
  let with_signal s f =
    let clone = S.map (fun x -> x) s in
    let kept = Keep.this clone in
    Lwt.finalize (fun () -> f clone)
                 (fun () -> S.stop clone; Keep.release kept; Lwt.return_unit)
  let with_event e f =
    let clone = E.map (fun x -> x) e in
    let kept = Keep.this clone in
    Lwt.finalize (fun () -> f clone)
                 (fun () -> E.stop clone; Keep.release kept; Lwt.return_unit)
end

Решая ваш пример с этим:

let run () =
  let switch = Lwt_switch.create () in

  let finished, waker = Lwt.wait () in
  let handler () = Lwt.wakeup waker () in
  (* We use [Lwt.async] because are not interested in knowing when exactly the reference will be released *)
  Lwt.async (fun () ->
    (React_utils.with_event (Lwt_react.E.map handler finished_event)
      (fun _dont_gc_me -> finished)));
  print_endline "Waiting for signal...";

  Gc.full_major ();  (* Force GC, to demonstrate problem *)
  fire_finished ();  (* Simulate send *)

  Lwt.bind finished (fun () -> Lwt_switch.turn_off switch)

Вот мой текущий (хакерский) обходной путь. Каждый обработчик добавляется в глобальную хеш-таблицу, а затем снова удаляется при выключении переключателя:

let keep =
  let kept = Hashtbl.create 10 in
  let next = ref 0 in
  fun ~switch value ->
    let ticket = !next in
    incr next;
    Hashtbl.add kept ticket value;
    Lwt_switch.add_hook (Some switch) (fun () ->
      Hashtbl.remove kept ticket;
      Lwt.return ()
    )

Используется так:

Lwt_react.E.map handler event |> keep ~switch;

Один из простых способов справиться с этим - сохранить ссылку на ваше мероприятие и позвонить React.E.stop когда ты больше не хочешь:

(* ocamlfind ocamlopt -package react,lwt,lwt.react,lwt.unix -linkpkg -o test test.ml *)

let finished_event, fire_finished = React.E.create ()

let run () =
  let switch = Lwt_switch.create () in

  let finished, waker = Lwt.wait () in
  let handler () = Lwt.wakeup waker () in
  let ev = Lwt_react.E.map handler finished_event in
  print_endline "Waiting for signal...";

  Gc.full_major ();  (* Force GC, to demonstrate problem *)
  fire_finished ();  (* Simulate send *)

  React.E.stop ev;

  Lwt.bind finished (fun () -> Lwt_switch.turn_off switch)

let () =
  Lwt_main.run (run ());
  print_endline "Done";

Обратите внимание, что если lwt не поддерживает отмену, то вы будете наблюдать то же поведение, заменив Lwt.protected (setup ()) от Lwt.bind (setup ()) Lwt.return,

В основном то, что у вас есть:

finished_event --weak--> SETUP --> finished

где SETUP это цикл между событием и потоком Lwt. Удаление Lwt.protected просто сдавливает последний указатель, и получается, что вы хотите.

У Lwt есть только прямые указатели (кроме отмены поддержки), а у React есть только обратные (слабые передние). Таким образом, способ заставить эту работу работать правильно - вернуть событие вместо потока.

Другие вопросы по тегам