Haskell lazy I/O y cierre de archivos
lazy-evaluation (7)
unsafeInterleaveIO?
Otra solución que me viene a la mente es usar unsafeInterleaveIO
de System.IO.Unsafe
. Vea la respuesta de Tomasz Zielonka en este hilo en Haskell Cafe.
Difiere una operación de entrada-salida (abrir un archivo) hasta que realmente se requiere. Por lo tanto, es posible evitar abrir todos los archivos a la vez y, en cambio, leerlos y procesarlos secuencialmente (abrirlos perezosamente).
Ahora, creo, mapM getFileLine
abre todos los archivos pero no comienza a leerlos hasta que se putStr . unlines
putStr . unlines
. Por lo tanto, muchos thunks con manejadores de archivos abiertos flotan alrededor, este es el problema. (Por favor, corríjame si estoy equivocado).
Un ejemplo
Un ejemplo modificado con unsafeInterleaveIO
está ejecutando en un directorio de 100 GB durante varios minutos, en espacio constante.
getList :: FilePath -> IO [String]
getList p =
let getFileLine path =
liftM (/c -> (show . md5 $ c) ++ " " ++ path)
(unsafeInterleaveIO $ BS.readFile path)
in mapM getFileLine =<< getRecursiveContents p
(Cambié para la implementación pureMD5 del hash)
PD: No estoy seguro de si este es un buen estilo. Creo que las soluciones con iteraciones y IO estrictas son mejores, pero esta es más rápida de hacer. Lo uso en scripts pequeños, pero temo confiar en un programa más grande.
He escrito un pequeño programa de Haskell para imprimir las sumas de comprobación MD5 de todos los archivos en el directorio actual (se busca recursivamente). Básicamente una versión de Haskell de md5deep
. Todo está bien y excelente, excepto si el directorio actual tiene una gran cantidad de archivos, en cuyo caso recibo un error como:
<program>: <currentFile>: openBinaryFile: resource exhausted (Too many open files)
Parece que la pereza de Haskell está causando que no cierre los archivos, incluso después de que se haya completado su línea de salida correspondiente.
El código relevante está abajo. La función de interés es 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.
¿Hay alguna idea sobre cómo podría resolver este problema?
El programa completo está disponible aquí: http://haskell.pastebin.com/PAZm0Dcb
Edición: tengo muchos archivos que no caben en la memoria RAM, por lo que no estoy buscando una solución que lea todo el archivo en la memoria de una vez.
EDITAR: lo siento, pensé que el problema era con los archivos, no con la lectura / recorrido. Ignora esto.
No hay problema, solo abra explícitamente el archivo (openFile), lea el contenido (Data.ByteString.Lazy.hGetContents), realice el hash md5 (let! H = md5 contents) y cierre el archivo explícitamente (hClose).
Edit: mi suposición era que el usuario estaba abriendo miles de archivos muy pequeños, resulta que son muy grandes. La pereza será esencial.
Bueno, necesitarás usar un mecanismo de IO diferente. Ya sea:
- IO estricta (procesar los archivos con Data.ByteString o System.IO.Strict
- o, Iteratee IO (solo para expertos en este momento).
También recomiendo encarecidamente que no se use ''desempaquetar'', ya que eso destruye el beneficio de usar secuencias de bytes.
Por ejemplo, puede reemplazar su perezoso IO con System.IO.Strict, dando como resultado:
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)
El problema es que mapM no es tan perezoso como parece: da como resultado una lista completa con un elemento por ruta de archivo. Y el archivo IO que está utilizando es perezoso, por lo que obtiene una lista con un archivo abierto por cada ruta de archivo.
La solución más sencilla en este caso es forzar la evaluación del hash para cada ruta de archivo. Una forma de hacerlo es con Control.Exception.evaluate
:
getFileLine path = do
theHash <- liftM (/c -> (hex $ hash $ BS.unpack c) ++ " " ++ path) (BS.readFile path)
evaluate theHash
Como han señalado otros, estamos trabajando en un reemplazo para el enfoque actual de la IO perezosa que es más general pero aún así simple.
Lazy IO es muy propenso a los errores.
Como Dons sugirió, debe utilizar estricto IO.
Puede usar una herramienta como Iteratee para ayudarlo a estructurar un código de IO estricto. Mi herramienta favorita para este trabajo son las listas monádicas.
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
Utilicé el paquete "pureMD5" aquí porque "Crypto" no parece ofrecer una implementación md5 de "transmisión".
Las listas ListT
/ ListT
provienen del paquete "List" en hackage (la lista T de transformers y mtl están ListT
y tampoco vienen con funciones útiles como takeWhile
)
No necesita utilizar ninguna forma especial de hacer IO, solo necesita cambiar el orden en el que hace las cosas. Entonces, en lugar de abrir todos los archivos y luego procesar el contenido, abre un archivo e imprime una línea de salida a la vez.
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
Por cierto, estoy usando un hash lib md5 diferente, la diferencia no es significativa.
Lo principal que está pasando aquí es la línea:
mapM_ (/path -> putStrLn . fileLine path =<< BS.readFile path)
Está abriendo un solo archivo, está consumiendo todo el contenido del archivo e imprimiendo una línea de salida. Cierra el archivo porque consume todo el contenido del archivo. Anteriormente, se demoraba cuando se consumía el archivo, lo que se retrasaba cuando se cerraba el archivo.
Si no está seguro de estar consumiendo todas las entradas pero quiere asegurarse de que el archivo se cierre de todas formas, puede usar la función withFile
de System.IO
:
mapM_ (/path -> withFile path ReadMode $ /hnd -> do
c <- BS.hGetContents hnd
putStrLn (fileLine path c))
La función withFile
abre el archivo y pasa el identificador de archivo a la función de cuerpo. Garantiza que el archivo se cierre cuando el cuerpo vuelva. Este patrón "withBlah" es muy común cuando se trata de recursos caros. Este patrón de recursos es directamente compatible con System.Exception.bracket
.
NOTA: He editado mi código ligeramente para reflejar los consejos en la respuesta de Duncan Coutts . Incluso después de esta edición, su respuesta es obviamente mucho mejor que la mía, y parece que no se queda sin memoria de la misma manera.
Aquí está mi rápido intento de una versión basada en Iteratee
. Cuando lo ejecuto en un directorio con aproximadamente 2,000 archivos pequeños (30-80K) es aproximadamente 30 veces más rápido que su versión aquí y parece que usa un poco menos de memoria.
Por alguna razón, parece que se queda sin memoria en archivos muy grandes; no comprendo Iteratee
suficientemente bien como para ser capaz de decir por qué con facilidad.
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)
Tenga en cuenta que necesitará el paquete iteratee y el pureMD5 de pureMD5
. (Y mis disculpas si he hecho algo horrible aquí, soy un principiante con esto).