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

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

: : 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, таким образом, я не ищу решение, которое читает весь файл в память сразу.

20
задан Jesse 16 September 2015 в 06:24
поделиться

7 ответов

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

Как предложил dons, вы должны использовать строгий 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 взяты из пакета "List" на hackage (ListT от transformers и mtl сломаны и не содержат полезных функций, таких как takeWhile)

11
ответ дан 29 November 2019 в 23:19
поделиться

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

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

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

Я бы также настоятельно рекомендовал не использовать 'unpack', так как это уничтожает преимущество использования байтовых строк.

Например, вы можете заменить ваш ленивый 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)
3
ответ дан 29 November 2019 в 23:19
поделиться

EDIT: извините, думал, что проблема в файлах, а не в чтении/обходе директории. Игнорируйте это.

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

0
ответ дан 29 November 2019 в 23:19
поделиться

unsafeInterleaveIO?

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

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

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

Пример

измененный пример с unsafeInterleaveIO теперь работает с каталогом 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. Я не уверен, что это хороший стиль. Я считаю, что решения с итерациями и строгим вводом-выводом лучше, но это быстрее сделать. Я использую его в небольших скриптах, но я бы побоялся полагаться на него в более крупной программе.

0
ответ дан 29 November 2019 в 23:19
поделиться

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

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

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

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

2
ответ дан 29 November 2019 в 23:19
поделиться

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

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 открывает файл и передает дескриптор файла функции тела. Это гарантирует, что файл будет закрыт, когда тело вернется. Этот шаблон withBlah очень распространен при работе с дорогими ресурсами. Этот шаблон ресурсов напрямую поддерживается System.Exception.bracket .

27
ответ дан 29 November 2019 в 23:19
поделиться

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


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

6
ответ дан 29 November 2019 в 23:19
поделиться
Другие вопросы по тегам:

Похожие вопросы: