如何在OCaml中创建大量线程?

如何在OCaml中创建大量线程?,ocaml,Ocaml,我在球拍组中找到了一个关于频道创建性能的问题 我想写一个OCaml的版本来测试 let post (c,x) = Event.sync (Event.send c x);; let accept c = Event.sync (Event.receive c);; let get_chan c = let n = accept c in print_int n;print_newline ();; let chan_trans (old_chan, new_chan) = let s

我在球拍组中找到了一个关于
频道
创建性能的问题

我想写一个OCaml的版本来测试

let post (c,x) = Event.sync (Event.send c x);;

let accept c = Event.sync (Event.receive c);;

let get_chan c = let n = accept c in print_int n;print_newline ();;

let chan_trans (old_chan, new_chan) =
  let s = accept old_chan in
  post (new_chan,(s+1));;

let rec whisper count init_val =
  let rec aux n chan =
    if n >= count then chan
    else
      let new_chan = Event.new_channel ()
      in Thread.create chan_trans (chan, new_chan);
      aux (n+1) new_chan
  in let leftest_chan = Event.new_channel ()
  in let t0 = Thread.create post (leftest_chan, init_val)
  in let rightest_chan = aux 0 leftest_chan
  in get_chan rightest_chan;;

whisper 10000 1;;
问题是,当我测试whisper 1000 1时,它产生了预期的
1001
。但是,当我尝试测试
whisper 10000 1
时,出现了如下错误

致命错误:异常系统错误(“Thread.create:资源暂时不可用”)

我使用这个命令编译并运行

ocamlc-thread unix.cma threads.cma-o prog-whiler.ml&&./prog-I +线程


OCaml线程模块使用真实的系统(内核)线程。线程总数受内核限制:

 cat /proc/sys/kernel/threads-max
 251422
当然,你可以增加这个

 echo 100000 > /proc/sys/kernel/threads-max
但更好的方法是将线程视为一种资源并相应地管理它们

let rec whisper count init_val =
  let rec aux n t chan =
    if n >= count then chan
    else
      let new_chan = Event.new_channel () in
      let t' = Thread.create chan_trans (chan, new_chan) in
      Thread.join t;
      aux (n+1) t' new_chan in
  let leftest_chan = Event.new_channel () in
  let t = Thread.create post (leftest_chan, init_val) in
  let rightest_chan = aux 0 t leftest_chan in
  get_chan rightest_chan
在这种情况下,它将与任何大小的管道一起运行。例如:

$ ocamlbuild -use-ocamlfind -tag thread -pkg threads ev.native
$ time ./ev.native 
100001

real    0m1.581s
但这种中国耳语的实施非常粗糙和低效。您不应该为此使用重量级本机线程(go也不使用它们)。相反,您应该使用来自或库的协作轻量级线程。这将是非常有效和美好的

用Lwt实现 这个实现与来自的Go实现非常相似,但我认为我们可以在OCaml中更高效、更简洁地实现这一点,而不必使用邮箱(但我不确定它是否符合基准测试的规则)

要与矿山机器上的Go实施进行比较:

$ go build whispers.go 
$ time ./whispers 
100001

real    0m0.952s
“缓慢”实施 上面的代码是对原始Go版本的完全诚实的重新实现。但它如此之快的原因之一是OCaml和Lwt非常聪明,尽管它创建了
100_000
线程和
100_001
通道,但没有线程会屈服于背景,因为每次
whisper
被称为通道时,该通道已经包含数据,因此线程处于就绪状态。因此,这只是一个创建线程和通道的有效循环。它可以在50毫秒内创建一百万个线程

因此,这是一种惯用且正确的做事方式。但为了真正的比较,让我们模仿一下行为。下面的实现将首先在堆中创建100_001通道和100_000线程,等待从左通道到右通道传输数据。只有在这之后,它才会将一个值放入最左边的通道,从而引发一系列反应。这将基本上模仿什么是正在发生的去引擎盖下

let whispers n =
  let rec loop i p =
    if i < n then
      let p' = Lwt_mvar.create_empty () in
      let _t =
        Lwt_mvar.take p >>= fun x ->
        Lwt_mvar.put p' (x+1) in
      loop (i+1) p'
    else Lwt_mvar.take p in
  let p0 = Lwt_mvar.create_empty () in
  let t = loop 1 p0 in
  Lwt_mvar.put p0 1 >>= fun () -> t

$ time ./lev.native
100001
real    0m0.111s
让我们窃窃私语=
让rec循环ip=
如果我>=乐趣x->
Lwt_mvar.将p'(x+1)放入
回路(i+1)p'
否则,请接受p
让p0=Lwt\u mvar.create\u empty()在
设t=回路1 p0 in
Lwt\u mvar.put p01>=fun()->t
$time./lev.native
100001
实0.111s

所以它稍微慢一点,事实上它比以前的实现慢了20倍(我使用了100万个线程来比较它们),但它仍然比Go快10倍

在阅读链接文章时,您可能想使用lwt,它是“OCaml的协作线程库”。结果如下所示:

let whisper left right =
  let%lwt n = Lwt_mvar.take right in
  Lwt_mvar.put left (n+1)

let main () =
  let n = 100_000 in
  let%lwt () = Lwt_io.printf "With %d mvars!\n" n in
  let leftmost = Lwt_mvar.create_empty () in
  let rec setup_whispers left i =
    if  i >= n
    then left
    else let right = Lwt_mvar.create_empty () in
      let () = Lwt.async (fun () -> whisper left right) in
      setup_whispers right (i+1) in
  let rightmost = setup_whispers leftmost 0 in
  let%lwt () = Lwt_mvar.put rightmost 1 in
  let%lwt res = Lwt_mvar.take leftmost in
  Lwt_io.printf "%d\n" res

let () = Lwt_main.run (main ())
然后编译并运行它

$ ocamlbuild -use-ocamlfind -pkg lwt,lwt.ppx,lwt.unix whisper.native
$ time ./whisper.native
With 100000 mvars!
100001

real    0m0.169s
user    0m0.156s
sys 0m0.008s

也许你想使用lwt,恐怕你的lwt实现是单线程的,或者至少在发送第一条消息之前没有生成所有的Whisper线程。这取决于你所称的线程。它将创建10万个线程,但不会将任何线程放在后台(调度它),因为Lwt很聪明,会注意到每个线程都准备好了。因此,根据规则,它将创建100000个线程和100001个通道,但在这个微基准上,我们只测量分配时间。这就是为什么它很快。顺便说一句,出于兴趣,我创建了一个慢实现,它有一个通道数组,与线程连接。它仍然非常快,比您的实现稍微慢一点。我相信这个练习的重点是构建10万个线程,它们都在等待消息。您的实现更类似于这个Go实现,它在我的机器@ReyCharles上运行约200毫秒,我更新了一个缓慢的实现,只是为了与Go进行公平比较。但我们都明白,这只是为了模仿Go调度器的行为。
let whisper left right =
  let%lwt n = Lwt_mvar.take right in
  Lwt_mvar.put left (n+1)

let main () =
  let n = 100_000 in
  let%lwt () = Lwt_io.printf "With %d mvars!\n" n in
  let leftmost = Lwt_mvar.create_empty () in
  let rec setup_whispers left i =
    if  i >= n
    then left
    else let right = Lwt_mvar.create_empty () in
      let () = Lwt.async (fun () -> whisper left right) in
      setup_whispers right (i+1) in
  let rightmost = setup_whispers leftmost 0 in
  let%lwt () = Lwt_mvar.put rightmost 1 in
  let%lwt res = Lwt_mvar.take leftmost in
  Lwt_io.printf "%d\n" res

let () = Lwt_main.run (main ())
$ ocamlbuild -use-ocamlfind -pkg lwt,lwt.ppx,lwt.unix whisper.native
$ time ./whisper.native
With 100000 mvars!
100001

real    0m0.169s
user    0m0.156s
sys 0m0.008s