【问题标题】:What's so bad about Lazy I/O?Lazy I/O 有什么不好?
【发布时间】:2011-08-19 01:22:18
【问题描述】:

我通常听说生产代码应避免使用延迟 I/O。我的问题是,为什么?除了玩弄之外,还可以使用 Lazy I/O 吗?是什么让替代品(例如枚举器)更好?

【问题讨论】:

    标签: haskell io lazy-evaluation


    【解决方案1】:

    Lazy IO 的问题是,释放您获得的任何资源在某种程度上是不可预测的,因为它取决于您的程序如何使用数据——它的“需求模式”。一旦您的程序删除了对该资源的最后一个引用,GC 最终将运行并释放该资源。

    惰性流是一种非常方便编程的风格。这就是为什么 shell 管道如此有趣和流行的原因。

    但是,如果资源受到限制(如在高性能场景中,或希望扩展到机器极限的生产环境中),则依靠 GC 进行清理可能是一个不充分的保证。

    有时您必须急切地释放资源,以提高可扩展性。

    那么有哪些惰性 IO 替代方案并不意味着放弃增量处理(这反过来又会消耗太多资源)?嗯,我们有基于 foldl 的处理,也称为迭代器或枚举器,由 Oleg Kiselyov in the late 2000s 引入,并被许多基于网络的项目推广。

    我们不是将数据作为惰性流或一个大批量处理,而是通过基于块的严格处理进行抽象,并保证在读取最后一个块后资源的最终确定。这就是基于迭代的编程的精髓,它提供了非常好的资源约束。

    基于迭代的 IO 的缺点是它有一个有点笨拙的编程模型(大致类似于基于事件的编程,而不是基于线程的控制)。在任何编程语言中,这绝对是一种先进的技术。而对于绝大多数编程问题,惰性IO完全可以满足。但是,如果您要打开许多文件,或者在许多套接字上进行通信,或者以其他方式同时使用许多资源,那么迭代器(或枚举器)方法可能是有意义的。

    【讨论】:

    • 因为我刚刚从关于惰性 I/O 的讨论中关注到这个老问题的链接,所以我想我应该添加一个注释,从那时起,迭代器的大部分尴尬已经被新的pipesconduit 等流媒体库。
    【解决方案2】:

    Dons 提供了一个很好的答案,但他忽略了(对我而言)迭代器最引人注目的特性之一:它们使空间管理的推理变得更容易,因为必须明确保留旧数据。考虑:

    average :: [Float] -> Float
    average xs = sum xs / length xs
    

    这是众所周知的空间泄漏,因为必须将整个列表 xs 保留在内存中才能计算 sumlength。通过创建折叠可以成为高效的消费者:

    average2 :: [Float] -> Float
    average2 xs = uncurry (/) <$> foldl (\(sumT, n) x -> (sumT+x, n+1)) (0,0) xs
    -- N.B. this will build up thunks as written, use a strict pair and foldl'
    

    但是对于每个流处理器都必须这样做有点不方便。有一些概括 (Conal Elliott - Beautiful Fold Zipping),但它们似乎没有流行起来。但是,迭代器可以为您提供类似级别的表达。

    aveIter = uncurry (/) <$> I.zip I.sum I.length
    

    这不如折叠有效,因为列表仍然会迭代多次,但是它是按块收集的,因此可以有效地对旧数据进行垃圾收集。为了破坏该属性,有必要显式保留整个输入,例如使用 stream2list:

    badAveIter = (\xs -> sum xs / length xs) <$> I.stream2list
    

    迭代器作为编程模型的状态是一项正在进行的工作,但它比一年前要好得多。我们正在了解哪些组合器有用(例如zipbreakEenumWith),哪些不太有用,结果内置的迭代器和组合器不断提供更多的表现力。

    也就是说,Dons 是正确的,他们是一种先进的技术;我当然不会对每个 I/O 问题都使用它们。

    【讨论】:

      【解决方案3】:

      我一直在生产代码中使用惰性 I/O。就像唐提到的那样,这只是在某些情况下才会出现的问题。但是只读取几个文件就可以了。

      【讨论】:

      • 我也使用惰性 I/O。当我想要更多地控制资源管理时,我会求助于迭代器。
      【解决方案4】:

      更新: 最近在 haskell-cafe Oleg Kiseljov showed 上,unsafeInterleaveST(用于在 ST monad 中实现惰性 IO)非常不安全 - 它破坏了等式推理。他表明它允许构造bad_ctx :: ((Bool,Bool) -&gt; Bool) -&gt; Bool 这样

      > bad_ctx (\(x,y) -> x == y)
      True
      > bad_ctx (\(x,y) -> y == x)
      False
      

      即使== 是可交换的。


      惰性 IO 的另一个问题:实际的 IO 操作可以推迟到为时已晚,例如在文件关闭之后。引用Haskell Wiki - Problems with lazy IO:

      例如,一个常见的初学者错误是在一个文件读完之前关闭它:

      wrong = do
          fileData <- withFile "test.txt" ReadMode hGetContents
          putStr fileData
      

      问题是 withFile 在强制 fileData 之前关闭句柄。正确的做法是把所有的代码都传给withFile:

      right = withFile "test.txt" ReadMode $ \handle -> do
          fileData <- hGetContents handle
          putStr fileData
      

      这里,数据在 withFile 完成之前被消费。

      这通常是意料之外且容易犯的错误。


      另请参阅:Three examples of problems with Lazy I/O

      【讨论】:

      • 实际上将hGetContentswithFile 结合起来是没有意义的,因为前者将句柄置于“伪关闭”状态并会(懒惰地)为您处理关闭,因此代码完全等同于@ 987654332@,甚至没有hCloseopenFile。这基本上就是惰性 I/O 。如果你不使用readFilegetContentshGetContents,你就没有使用惰性 I/O。例如line &lt;- withFile "test.txt" ReadMode hGetLine 工作正常。
      • @Dag:虽然hGetContents 会为您处理关闭文件,但也可以自己“提前”关闭它,这有助于确保资源以可预测的方式释放。
      【解决方案5】:

      到目前为止还没有提到惰性 IO 的另一个问题是它具有令人惊讶的行为。在一个普通的 Haskell 程序中,有时很难预测程序的每个部分何时被评估,但幸运的是,由于纯度,除非你有性能问题,否则它真的无关紧要。当引入惰性 IO 时,代码的评估顺序实际上会影响其含义,因此您习惯认为无害的更改可能会给您带来真正的问题。

      例如,这里有一个关于代码的问题,看起来很合理,但由于延迟 IO 而变得更加混乱:withFile vs. openFile

      这些问题并不总是致命的,但这是另外一回事,而且我个人会避免懒惰的 IO,除非预先完成所有工作存在真正的问题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-11-12
        • 2015-07-05
        • 2010-09-27
        • 2015-01-17
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多