Haskell懒惰的I / O和关闭文件

时间:2010-06-05 18:43:22

标签: haskell lazy-evaluation

我编写了一个小的Haskell程序来打印当前目录中所有文件的MD5校验和(递归搜索)。基本上是md5deep的Haskell版本。一切都很好,花花公子,除非当前目录有大量的文件,在这种情况下,我得到一个错误:

<program>: <currentFile>: openBinaryFile: resource exhausted (Too many open files)

似乎Haskell的懒惰导致它不会关闭文件,即使在相应的输出行已经完成之后也是如此。

相关代码如下。感兴趣的功能是getList

import qualified Data.ByteString.Lazy as BS

main :: IO ()
main = putStr . unlines =<< getList "."

getList :: FilePath -> IO [String]
getList p =
    let getFileLine path = liftM (\c -> (hex $ hash $ BS.unpack c) ++ " " ++ path) (BS.readFile path)
    in mapM getFileLine =<< getRecursiveContents p

hex :: [Word8] -> String
hex = concatMap (\x -> printf "%0.2x" (toInteger x))

getRecursiveContents :: FilePath -> IO [FilePath]
-- ^ Just gets the paths to all the files in the given directory.

关于如何解决这个问题有什么想法吗?

整个计划可在此处获取:http://haskell.pastebin.com/PAZm0Dcb

编辑:我有大量不适合RAM的文件,所以我不是在寻找一种能够将整个文件一次性读入内存的解决方案。

7 个答案:

答案 0 :(得分:27)

您无需使用任何特殊的IO方式,只需更改执行操作的顺序即可。因此,不是打开所有文件然后处理内容,而是打开一个文件并一次打印一行输出。

import Data.Digest.Pure.MD5 (md5)
import qualified Data.ByteString.Lazy as BS

main :: IO ()
main = mapM_ (\path -> putStrLn . fileLine path =<< BS.readFile path) 
   =<< getRecursiveContents "."

fileLine :: FilePath -> BS.ByteString -> String
fileLine path c = hash c ++ " " ++ path

hash :: BS.ByteString -> String 
hash = show . md5
顺便说一下,我碰巧使用的是另一个md5哈希库,区别并不显着。

这里发生的主要事情是:

mapM_ (\path -> putStrLn . fileLine path =<< BS.readFile path)

它正在打开一个文件,它占用了文件的全部内容并打印了一行输出。它会关闭文件,因为它占用了文件的全部内容。以前,当文件被占用时,你会延迟文件被关闭时延迟。

如果您不确定是否正在使用所有输入但想要确保文件仍然关闭,那么您可以使用withFile中的System.IO函数:

mapM_ (\path -> withFile path ReadMode $ \hnd -> do
                  c <- BS.hGetContents hnd
                  putStrLn (fileLine path c))

withFile函数打开文件并将文件句柄传递给body函数。它保证在正文返回时文件被关闭。在处理昂贵的资源时,这种“withBlah”模式非常普遍。 System.Exception.bracket直接支持此资源模式。

答案 1 :(得分:11)

懒惰的IO很容易出错。

正如dons建议的那样,你应该使用严格的IO。

您可以使用Iteratee等工具来帮助您构建严格的IO代码。我最喜欢的工作是monadic列表。

import Control.Monad.ListT (ListT) -- List
import Control.Monad.IO.Class (liftIO) -- transformers
import Data.Binary (encode) -- binary
import Data.Digest.Pure.MD5 -- pureMD5
import Data.List.Class (repeat, takeWhile, foldlL) -- List
import System.IO (IOMode(ReadMode), openFile, hClose)
import qualified Data.ByteString.Lazy as BS
import Prelude hiding (repeat, takeWhile)

hashFile :: FilePath -> IO BS.ByteString
hashFile =
    fmap (encode . md5Finalize) . foldlL md5Update md5InitialContext . strictReadFileChunks 1024

strictReadFileChunks :: Int -> FilePath -> ListT IO BS.ByteString
strictReadFileChunks chunkSize filename =
    takeWhile (not . BS.null) $ do
        handle <- liftIO $ openFile filename ReadMode
        repeat () -- this makes the lines below loop
        chunk <- liftIO $ BS.hGet handle chunkSize
        when (BS.null chunk) . liftIO $ hClose handle
        return chunk

我在这里使用了“pureMD5”软件包,因为“Crypto”似乎没有提供“流式”md5实现。

monadic列表/ ListT来自hackage上的“List”包(变形金刚'和mtl的ListT已被破坏,并且没有像takeWhile这样的有用函数<) p>

答案 2 :(得分:6)

注意:我已略微编辑了我的代码以反映Duncan Coutts's answer中的建议。即使在这次编辑之后,他的回答显然比我的好得多,并且似乎没有以同样的方式耗尽内存。


这是我对基于Iteratee的版本的快速尝试。当我在一个包含大约2,000个小(30-80K)文件的目录上运行它时,它比your version here快大约30倍,并且似乎使用了更少的内存。

出于某种原因,它似乎仍然在非常大的文件上耗尽内存 - 我还不太了解Iteratee,但却无法轻易地告诉原因。

module Main where

import Control.Monad.State
import Data.Digest.Pure.MD5
import Data.List (sort)
import Data.Word (Word8) 
import System.Directory 
import System.FilePath ((</>))
import qualified Data.ByteString.Lazy as BS

import qualified Data.Iteratee as I
import qualified Data.Iteratee.WrappedByteString as IW

evalIteratee path = evalStateT (I.fileDriver iteratee path) md5InitialContext

iteratee :: I.IterateeG IW.WrappedByteString Word8 (StateT MD5Context IO) MD5Digest
iteratee = I.IterateeG chunk
  where
    chunk s@(I.EOF Nothing) =
      get >>= \ctx -> return $ I.Done (md5Finalize ctx) s
    chunk (I.Chunk c) = do
      modify $ \ctx -> md5Update ctx $ BS.fromChunks $ (:[]) $ IW.unWrap c
      return $ I.Cont (I.IterateeG chunk) Nothing

fileLine :: FilePath -> MD5Digest -> String
fileLine path c = show c ++ " " ++ path

main = mapM_ (\path -> putStrLn . fileLine path =<< evalIteratee path) 
   =<< getRecursiveContents "."

getRecursiveContents :: FilePath -> IO [FilePath]
getRecursiveContents topdir = do
  names <- getDirectoryContents topdir

  let properNames = filter (`notElem` [".", ".."]) names

  paths <- concatForM properNames $ \name -> do
    let path = topdir </> name

    isDirectory <- doesDirectoryExist path
    if isDirectory
      then getRecursiveContents path
      else do
        isFile <- doesFileExist path
        if isFile
          then return [path]
          else return []

  return (sort paths)

concatForM :: (Monad m) => [a1] -> (a1 -> m [a]) -> m [a]
concatForM xs f = liftM concat (forM xs f)

请注意,您需要iteratee包和TomMD的pureMD5。 (如果我在这里做了一些可怕的事情,我很抱歉 - 我是这个人的初学者。)

答案 3 :(得分:3)

编辑:我的假设是用户打开了数千个非常小的文件,结果发现它们非常大。懒惰将是至关重要的。

好吧,你需要使用不同的IO机制。之一:

  • 严格IO(使用Data.ByteString或System.IO.Strict
  • 处理文件
  • 或Iteratee IO(目前仅供专家使用)。

我也强烈建议不要使用'unpack',因为这会破坏使用字节串的好处。

例如,您可以使用System.IO.Strict替换惰性IO,并产生:

import qualified System.IO.Strict as S

getList :: FilePath -> IO [String]
getList p = mapM getFileLine =<< getRecursiveContents p
    where
        getFileLine path = liftM (\c -> (hex (hash c)) ++ " " ++ path)
                                 (S.readFile path)

答案 4 :(得分:2)

问题是mapM并不像你想象的那样懒惰 - 它会产生一个完整的列表,每个文件路径有一个元素。您使用的文件IO 是懒惰的,因此您会得到一个列表,其中每个文件路径都有一个打开的文件。

在这种情况下,最简单的解决方案是强制评估每个文件路径的哈希值。一种方法是使用Control.Exception.evaluate

getFileLine path = do
  theHash <- liftM (\c -> (hex $ hash $ BS.unpack c) ++ " " ++ path) (BS.readFile path)
  evaluate theHash

正如其他人所指出的那样,我们正在努力替换当前的懒惰IO方法,这种方法更为通用但仍然很简单。

答案 5 :(得分:0)

编辑:对不起,以为问题出在文件上,而不是阅读/遍历。忽略这一点。

没问题,只需显式打开文件(openFile),读取内容(Data.ByteString.Lazy.hGetContents),执行m​​d5哈希(让!h = md5内容),并显式关闭文件(hClose)。

答案 6 :(得分:0)

unsafeInterleaveIO?

我想到的另一个解决方案是使用unsafeInterleaveIO中的System.IO.Unsafe。请参阅Haskell Cafe的this thread中Tomasz Zielonka的回复。

它推迟输入输出操作(打开文件),直到实际需要它为止。因此,可以避免一次打开所有文件,而是按顺序读取和处理它们(懒惰地打开它们)。

现在,我相信,mapM getFileLine会打开所有文件,但在putStr . unlines之前不会开始阅读这些文件。因此很多带有打开文件处理程序的thunk都会浮动,这就是问题所在。 (如果我错了,请纠正我。)

一个例子

modified example with unsafeInterleaveIO现在在一个100 GB的目录中运行几分钟,在恒定的空间内。

getList :: FilePath -> IO [String]
getList p =
  let getFileLine path =
        liftM (\c -> (show . md5 $ c) ++ " " ++ path)
        (unsafeInterleaveIO $ BS.readFile path)
  in mapM getFileLine =<< getRecursiveContents p 

(我为hash的pureMD5实现改了)

P.S。我不确定这是不是好风格。我相信具有迭代和严格IO的解决方案更好,但这个更快。我在小脚本中使用它,但我害怕在更大的程序中依赖它。