为什么这个基于Lwt且看似并发的代码如此不一致

时间:2015-07-01 06:07:22

标签: ocaml lwt

我正在尝试创建Lwt的并发示例,并提出了这个小样本

let () =
  Lwt_main.run (
      let start = Unix.time () in
      Lwt_io.open_file Lwt_io.Input "/dev/urandom" >>= fun data_source ->
      Lwt_unix.mkdir "serial" 0o777 >>= fun () ->
      Lwt_list.iter_p
        (fun count ->
         let count = string_of_int count in
         Lwt_io.open_file
           ~flags:[Unix.O_RDWR; Unix.O_CREAT]
           ~perm:0o777
           ~mode:Lwt_io.Output ("serial/file"^ count ^ ".txt") >>= fun h ->
         Lwt_io.read ~count:52428800
                     data_source >>= Lwt_io.write_line h)
        [0;1;2;3;4;5;6;7;8;9] >>= fun () ->
      let finished = Unix.time () in
      Lwt_io.printlf "Execution time took %f seconds" (finished -. start))
编辑:要求50GB,它是:       “然而,这非常缓慢,基本上没用。       是否需要以某种方式强制内部绑定?“

编辑:我最初写过要求50 GB并且它从未完成,现在我有一个不同的问题,要求50MB,执行几乎是瞬间,而du -sh报告的目录大小只有80k。

编辑:我也尝试使用明确关闭文件句柄的代码,但结果相同。

我在OS X最新版本并使用

进行编译

ocamlfind ocamlopt -package lwt.unix main.ml -linkpkg -o Test

(我也试过/dev/random,是的,我正在使用挂钟时间。)

2 个答案:

答案 0 :(得分:17)

因此,您的代码存在一些问题。

第1期

主要问题是您错误地理解了Lwt_io.read函数(没有人可以怪你!)。

val read : ?count : int -> input_channel -> string Lwt.t
  (** [read ?count ic] reads at most [len] characters from [ic]. It
      returns [""] if the end of input is reached. If [count] is not
      specified, it reads all bytes until the end of input. *)

指定~count:len后,它会读取最多 len个字符。至多意味着它可以减少阅读量。但是如果省略count选项,那么它将读取所有数据。我个人认为这种行为不直观,即使不是很奇怪。因此,此最多意味着最多len或更少,即,不保证它将准确读取len个字节。事实上,如果你在你的程序中添加一张支票:

 Lwt_io.read ~count:52428800 data_source >>= fun data ->
 Lwt_io.printlf "Read %d bytes" (String.length data) >>= fun () ->
 Lwt_io.write h data >>= fun () ->

您将看到,每次尝试它只会读取4096个字节:

Read 4096 bytes
Read 4096 bytes
Read 4096 bytes
Read 4096 bytes
Read 4096 bytes
Read 4096 bytes
Read 4096 bytes
Read 4096 bytes
Read 4096 bytes
Read 4096 bytes

为什么4096?因为这是默认的缓冲区大小。但它实际上并不重要。

第2期

Lwt_io模块实现了缓冲IO。这意味着您的所有写入和读取都不会直接进入文件,而是缓存在内存中。这意味着,您应该记得flushclose。您的代码在完成时不会关闭描述符,因此您可能会遇到在程序终止后某些缓冲区未刷新的情况。特别是Lwt_io,在程序退出之前刷新所有缓冲区。但是你不应该依赖这个没有文档记录的功能(将来你可能会遇到任何其他缓冲的io,比如来自标准C库的fstream)。所以,总是关闭你的文件(另一个问题是,今天的文件描述符是最宝贵的资源,它们的泄漏很难找到。)

第3期

请勿使用/dev/urandom/dev/random来衡量io。对于前者,您将测量随机数生成器的性能,对于后者,您将测量机器中的熵流。两者都很慢。根据CPU的速度,您很少会获得超过16 Mb / s的速度,而且要小得多,那么Lwt可以吞吐量。从/dev/zero读取并写入/dev/null实际上将执行内存中的所有传输,并显示程序可以实现的实际速度。一个编写良好的程序仍将受到内核速度的限制。在下面提供的示例程序中,这将显示平均速度为700 MB / s。

第4期

如果你真的在努力表现,请不要使用缓冲的io。你永远不会得到最大化。例如,Lwt_io.read将首先在缓冲区读取,然后它将创建string并将数据复制到该字符串。如果你真的需要一些性能,那么你应该提供自己的缓冲。在大多数情况下,没有必要这样做,因为Lwt_io非常有效。但是如果你需要每秒处理几十兆字节,或者需要一些特殊的缓冲策略(非线性),你可能需要考虑提供自己的缓冲。好消息是Lwt_io允许你这样做。您可以查看example程序,该程序将衡量Lwt输入/输出的性能。它模仿着名的pv计划。

第5期

您希望通过并行运行线程来获得一些性能。问题是在你的测试中没有并发的地方。 /dev/random(以及/dev/zero)是一个仅受CPU限制的设备。这与调用random函数一样。它始终为available,因此系统调用不会阻止它。写入常规文件也不是并发的好地方。首先,通常只有一个硬盘驱动器,其中有一个写入头。即使系统调用将阻塞并将控制权交给另一个线程,这也会导致性能偏离,因为两个线程现在将竞争标头位置。如果你有SSD,那么标题就不会有任何竞争,但性能会更差,因为你会破坏你的缓存。但幸运的是,通常写常规文件并不会阻止。因此,您的线程将会运行,即它们将被序列化。

答案 1 :(得分:3)

如果你查看你的文件,你会看到他们每个4097K - 从/ dev / urandom读取的4096K,以及换行符的一个字节。你用Lwt_io.read来达到最大缓冲区,所以即使你说〜计数:awholelot,它只给你〜计数:4096。

我不知道规范Lwt的做法是什么,但这是另一种选择:

open Lwt

let stream_a_little source n = 
    let left = ref n in
    Lwt_stream.from (fun () -> 
        if !left <= 0 then return None
        else Lwt_io.read ~count:!left source >>= (fun s -> 
            left:=!left - (Bytes.length s);
            return (Some s)
        ))

let main () =
    Lwt_io.open_file ~buffer_size:(4096*8) ~mode:Lwt_io.Input "/dev/urandom" >>= fun data_source ->
        Lwt_unix.mkdir "serial" 0o777 >>= fun () ->
            Lwt_list.iter_p
        (fun count ->
            let count = string_of_int count in
            Lwt_io.open_file
           ~flags:[Unix.O_RDWR; Unix.O_CREAT]
           ~perm:0o777
           ~mode:Lwt_io.Output ("serial/file"^ count ^ ".txt") >>= (fun h ->
               Lwt_stream.iter_s (Lwt_io.write h)
               (stream_a_little data_source 52428800)))
        [0;1;2;3;4;5;6;7;8;9]

let timeit f =
        let start = Unix.time () in
        f () >>= fun () ->
            let finished = Unix.time () in
            Lwt_io.printlf "Execution time took %f seconds" (finished -. start)

let () =
    Lwt_main.run (timeit main)

编辑:请注意,lwt是合作线程库;当你有两个线程同时进行时,他们实际上并没有同时在你的OCaml进程中做任何事情。 OCaml(目前)是单核的,所以当一个线程正在移动时,其他线程会很好地等待,直到该线程说“#34;好吧,我做了一些工作,其他人去了#34;”。因此,当您尝试同时传输到8个文件时,您基本上会将一些随机性发送到file1,然后稍微提交到file2,...稍微提交到文件8,然后(如果还有工作)如果您还等待大量输入(比如您的输入是通过网络传输),那么这对于文件1,然后一点到文件2等都是有意义的。并且您的主要流程有很多是时候浏览每个线程并检查&#34;是否有任何输入?&#34;,但是当你所有的线程只是从/ dev / random读取时,只需填写一个文件就快得多首先,然后是第二个,等等。并假设几个CPU可以并行读取/ dev /(u)随机(并且你的驱动器可以跟上),它当然是同时加载ncpu读取要快得多,但是你需要多核(或者只是在shell脚本中执行此操作)。

EDIT2:展示了如何增加阅读器的缓冲区大小,提高速度;)请注意,您也可以在旧示例中​​将buffer_size设置为您想要的高度,这样可以一次性读取所有内容,但除非你多次阅读,否则你得不到你的buffer_size。