线程的定界连续性与Lwt
Delimited Continuations vs. Lwt for Threads

原始链接: https://mirageos.org/blog/delimcc-vs-lwt

## MirageOS 并发:Lwt vs. Fibers MirageOS 是一种独特的操作系统内核,采用完全事件驱动的方法,摒弃了传统的抢占式线程。这需要抽象来管理事件回调,目前 Lwt – 一个采用单子风格的 OCaml 线程库 – 是主要的解决方案。虽然有效,但 Lwt 的单子特性需要代码适配,并且为阻塞操作引入了闭包分配,这引发了 OCaml 社区的讨论。 像通过 Lwt_fiber 库提供的限定连续 (delimcc) 这样的替代方案,提供了一种潜在的更轻量级的方法,使用可重启的异常。比较 Lwt 和 fibers 的基准测试表明,对于简单的阻塞场景,fibers 最初的性能*不如* Lwt,因为异常处理的开销。然而,随着更深的调用栈和减少的阻塞频率,fibers 表现出更好的性能,避免了重复的闭包分配。 最终,性能差异对于 MirageOS 的核心需求来说似乎并不重要。Lwt 的关键优势在于它可以无缝编译到 Javascript,通过 js_of_ocaml 实现,从而实现浏览器可视化以及与 Node.js 的互操作性。选择更多地取决于代码互操作性以及其他语言(如 Javascript 的 `yield` 运算符)中并发功能的发展,而不是原始速度。

黑客新闻 新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 分隔符连续性 vs. Lwt 用于线程 (mirageos.org) 4 点赞 romes 1 小时前 | 隐藏 | 过去 | 收藏 | 讨论 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

MirageOS is a fully event-driven system, with no support for conventional preemptive threads. Instead, programs are woken by events such as incoming network packets, and event callbacks execute until they themselves need to block (due to I/O or timers) or complete their task.

Event-driven systems are simple to implement, scalable to lots of network clients, and very hip due to frameworks like node.js. However, programming event callbacks directly leads to the control logic being scattered across many small functions, and so we need some abstractions to hide the interruptions of registering and waiting for an event to trigger.

OCaml has the excellent Lwt threading library that utilises a monadic approach to solving this. Consider this simplified signature:

  val return : 'a -> 'a Lwt.t 
  val bind : 'a Lwt.t -> ('a -> 'b Lwt.t) -> 'b Lwt.t
  val run : 'a Lwt.t -> 'a

Threads have the type 'a Lwt.t, which means that the thread will have a result of type 'a when it finishes. The return function is the simplest way to construct such a thread from an OCaml value.

If we then wish to use the value of thread, we must compose a function that will be called in the future when the thread completes. This is what the bind function above is for. For example, assume we have a function that will let us sleep for some time:

  val sleep: int -> unit Lwt.t

We can now use the bind function to do something after the sleep is complete:

  let x = sleep 5 in
  let y = bind x (fun () -> print_endline "awake!") in
  run y

x has the type unit Lwt.t, and the closure passed to bind will eventually be called with unit when the sleep finishes. Note that we also need a function to actually begin evaluating an Lwt thread, which is the run function.

2011 OCaml Users Group meeting that Lwt is not to everyone's tastes. There are a few issues:

  • The monadic style means that existing code will not just work. Any code that might block must be adapted to use return and bind, which makes integrating third-party code problematic.

  • More concerningly, any potential blocking points require the allocation of a closure. This allocation is very cheap in OCaml, but is still not free. Jun Furuse notes that combinator-based systems are slower during the development of his Planck parser.

Lwt addresses the first problem via a comprehensive syntax extension which provides Lwt equivalents for many common operations. For example, the above example with sleep can be written as:

  lwt x = sleep 5 in
  print_endline "awake"

The lwt keyword indicates the result of the expression should be passed through bind, and this makes it possible to write code that looks more OCaml-like. There are also other keywords like for_lwt and match_lwt that similarly help with common control flow constructs.

delimcc library which implements delimited continuations for OCaml. These can be used to implement restartable exceptions: a program can raise an exception which can be invoked to resume the execution as if the exception had never happened. Delimcc can be combined with Lwt very elegantly, and Jake Donham did just this with the Lwt_fiber library. His post also has a detailed explanation of how delimcc works.

The interface for fibers is also simple:

  val start: (unit -> 'a) -> 'a Lwt.t
  val await : 'a Lwt.t -> 'a

A fiber can be launched with start, and during its execution can block on another thread with await. When it does block, a restartable exception saves the program stack back until the point that start was called, and it will be resumed when the thread it blocked on completes.

I need to stress that these benchmarks are very micro, and do not take into account other things like memory allocation. The standalone code for the tests is online at Github, and I would be delighted to hear any feedback.

Javascript 1.7 introduces a yield operator, which has been shown to have comparable expressive power to the shift-reset delimcc operators. Perhaps convergence isn't too far away after all...

联系我们 contact @ memedata.com