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