Подтвердить что ты не робот

Haskell ленивый ввод-вывод и закрытие файлов

Я написал небольшую программу Haskell для печати контрольных сумм MD5 всех файлов в текущем каталоге (рекурсивно поиск). В основном версия Haskell md5deep. Все отлично и dandy, за исключением случаев, когда в текущем каталоге имеется очень большое количество файлов, и в этом случае я получаю ошибку, например:

<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

Изменить: У меня есть много файлов, которые не вписываются в ОЗУ, поэтому я не ищу решение, которое сразу считывает весь файл.

4b9b3361

Ответ 1

Lazy IO очень подвержен ошибкам.

Как предполагалось, вы должны использовать строгий IO.

Вы можете использовать такой инструмент, как Iteratee, чтобы помочь вам структурировать строгий код ввода-вывода. Моим любимым инструментом для этой работы являются монадические списки.

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.

Монадические списки / ListT поступают из пакета "Список" при взломе (трансформаторы и mtl ListT разбиты, а также не имеют полезных функций, таких как takeWhile)

Ответ 2

Вам не нужно использовать какой-либо специальный способ ввода 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.

Ответ 3

ПРИМЕЧАНИЕ. Я немного изменил свой код, чтобы отразить рекомендации Duncan Coutts. Даже после этого редактирования его ответ, очевидно, намного лучше, чем мой, и похоже, что он не исчерпывает память таким же образом.


Вот моя быстрая попытка использования версии Iteratee. Когда я запускаю его в каталоге с примерно 2000 небольшими (30-80K) файлами, он примерно в 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 [email protected](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. (И мои извинения, если я сделал что-то ужасное здесь - я начинаю с этим материалом.)

Ответ 4

Изменить: мое предположение состояло в том, что пользователь открывал тысячи очень маленьких файлов, оказалось, что они очень большие. Лень будет необходима.

Ну, вам нужно использовать другой механизм ввода-вывода. Или:

  • Строгий IO (обрабатывать файлы с помощью Data.ByteString или System.IO.Strict
  • или Iteratee IO (для экспертов только в данный момент).

Я также настоятельно рекомендую не использовать "распаковать", поскольку это разрушает преимущество использования байтов.

Например, вы можете заменить свой ленивый IO на System.IO.Strict, уступая:

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)

Ответ 5

Проблема заключается в том, что mapM не такой ленивый, как вы думаете, - это приводит к полному списку с одним элементом в каждом пути к файлу. И файл IO, в котором вы используете , ленив, поэтому вы получаете список с одним открытым файлом на каждый путь к файлу.

Простейшим решением в этом случае является принудительная оценка хэша для каждого пути к файлу. Один из способов сделать это: Control.Exception.evaluate:

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

Как отмечали другие, мы работаем над заменой текущего подхода к ленивому IO, который является более общим, но все же простым.

Ответ 6

РЕДАКТИРОВАТЬ: извините, подумал, что проблема связана с файлами, а не с диалектом чтения/обхода. Игнорируйте это.

Нет проблем, просто явно открыть файл (openFile), прочитать содержимое (Data.ByteString.Lazy.hGetContents), выполнить хеш файл md5 (let! h = md5) и явно закрыть файл (hclose).

Ответ 7

unsafeInterleaveIO?

Еще одно решение, которое приходит на ум, - использовать unsafeInterleaveIO из System.IO.Unsafe. См. Ответ Томаша Зеленко в этот поток в Haskell Cafe.

Он отменяет операцию ввода-вывода (открытие файла), пока она не понадобится. Таким образом, можно избежать одновременного открытия всех файлов и вместо этого читать и обрабатывать их последовательно (открывать их лениво).

Теперь, я считаю, mapM getFileLine открывает все файлы, но не начинает считывать их до putStr . unlines. Таким образом, много thunks с открытыми обработчиками файлов плавают вокруг, это проблема. (Пожалуйста, поправьте меня, если я ошибаюсь).

Пример

A измененный пример с unsafeInterleaveIO работает в течение 100 минут в каталоге 100 ГБ в постоянном пространстве.

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

(я изменил реализацию pureMD5 хэша)

P.S. Я не уверен, что это хороший стиль. Я считаю, что решения с iteretees и строгими IO лучше, но это быстрее сделать. Я использую его в небольших скриптах, но я бы боялся полагаться на него в более крупной программе.