【问题标题】:How to stop OCaml garbage collecting my reactive event handler?如何停止 OCaml 垃圾收集我的反应事件处理程序?
【发布时间】:2013-11-14 10:24:00
【问题描述】:

我正在尝试将OBus 库与Lwt_react 一起使用。这对属性和信号使用“函数式反应式编程”。

问题(如React documentation 中所述)是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 取消而掩盖问题。

【问题讨论】:

    标签: garbage-collection ocaml reactive-programming ocaml-lwt


    【解决方案1】:

    这是从我的某个项目中获取的 sn-p,已修复以解决此弱引用问题(谢谢!)。 第一部分是保持全局根指向您的对象。 第二部分是将信号/事件的活跃度限制在 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)
    

    【讨论】:

    • 我认为这是解决相反的问题。一旦你的函数完成,你想停止接收事件。我想继续接收事件,直到它完成。我现在用一些适当的示例代码更新了这个问题来演示这个问题。我看不到使用您的模块修复它的方法。谢谢,
    • 我不同意。我想在线程完成之前接收事件,并在完成后停止接收它们。据我了解,第一部分正是您的目标。
    • 好的,确实有效!但是出于一个非常不明显的原因。我最终通过查看 Lwt 源代码弄明白了。 Lwt.bind 调用创建了一个从最终任务返回到“完成”的额外引用,因为它是一个“可取消”任务(取消“运行”的结果将取消“完成”)。不过,我担心这有点脆弱。例如,在Gc 行之前添加let finished = Lwt.protected finished in 会再次中断它。
    • 你说得对,它不起作用,但必须有办法解决这个问题……为什么它一开始似乎起作用对我来说很明显:lwt 线程保持对反应图的引用,因此在 Reactive_utils 创建的 lwt 线程完成之前不能被垃圾收集。
    • 为什么它不起作用是有趣和微妙的恕我直言......我认为这是因为所有保持到完成的引用都很弱:“waker”在 React 事件中被引用,它很弱。 "finished" 被本地范围引用,但在 Lwt.protected 调用之后,本地范围不再可以访问原始的finished。由于对已完成线程的两个可能引用很弱,因此它们被垃圾收集,以及 React_utils 对它们所做的所有管道。我们需要一种使线程“强”的方法,这意味着在确定之前保持对它的强引用
    【解决方案2】:

    这是我当前的(hacky)解决方法。每个处理程序都被添加到全局哈希表中,然后在关闭开关时再次删除:

    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;
    

    【讨论】:

      【解决方案3】:

      处理此问题的一种简单方法是保留对您的事件的引用,并在您不再需要它时致电 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";
      

      【讨论】:

      • 这是我糟糕的测试用例的错(将真实代码与测试驱动程序代码混合在一起)。我已经用更好的版本替换了它。在实际代码中,您不能在fire_finished 之后立即调用stop,因为fire_finished 发生在事件源处,而不是接收器处。如果您将stop 放在bind 函数中,您将遇到与@Def 的答案相同的问题;它可能会被垃圾收集。
      • 我明白了。那么这是预料之中的。
      【解决方案4】:

      请注意,如果 lwt 不支持取消,那么您将通过将 Lwt.protected (setup ()) 替换为 Lwt.bind (setup ()) Lwt.return 来观察相同的行为。

      基本上你拥有的是:

      finished_event --weak--&gt; SETUP --&gt; finished

      其中SETUP 是事件和Lwt 线程之间的循环。删除 Lwt.protected 只是 squashes 最后一个指针,所以它恰好可以做你想做的事情。

      Lwt 只有前向指针(支持取消除外),而 React 只有后向指针(前向指针很弱)。所以要让它正常工作的方法是返回事件而不是线程。

      【讨论】:

      • 好的,但是如果我返回一个事件,那么我不能将它传递给 Lwt_main.run,或者其他任何需要任务的东西,对吧? (这一切都发生在一个较大程序的一个小子例程中 - 特别是 0install 执行 PackageKit 事务)
      猜你喜欢
      • 2010-09-22
      • 1970-01-01
      • 2011-07-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多