Frage Haskell Lazy I / O und Schließen von Dateien


Ich habe ein kleines Haskell-Programm geschrieben, um die MD5-Prüfsummen aller Dateien im aktuellen Verzeichnis zu drucken (rekursiv recherchiert). Grundsätzlich eine Haskell-Version von md5deep. Alles ist gut und schön, außer wenn das aktuelle Verzeichnis eine sehr große Anzahl von Dateien enthält. In diesem Fall bekomme ich einen Fehler wie:

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

Es scheint, dass Haskells Faulheit dazu führt, dass Dateien nicht geschlossen werden, selbst nachdem die entsprechende Ausgabezeile fertiggestellt wurde.

Der entsprechende Code ist unten. Die Funktion von Interesse ist 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.

Gibt es Ideen, wie ich dieses Problem lösen könnte?

Das gesamte Programm ist hier verfügbar: http://haskell.pastebin.com/PAZm0Dcb

Bearbeiten: Ich habe viele Dateien, die nicht in RAM passen, also suche ich nicht nach einer Lösung, die die gesamte Datei gleichzeitig in den Speicher liest.


19
2018-06-05 18:43


Ursprung


Antworten:


Lazy IO ist sehr anfällig für Fehler.

Wie Dons vorgeschlagen, sollten Sie strenge IO verwenden.

Sie können ein Tool wie Iteratee verwenden, um den strikten IO-Code zu strukturieren. Mein Lieblingswerkzeug für diesen Job sind monadische Listen.

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

Ich habe das "pureMD5" -Paket hier verwendet, weil "Crypto" keine "Streaming" -MD5-Implementierung zu bieten scheint.

Monadische Listen /ListT kommen aus dem "Liste" -Paket auf Hacker (Transformatoren und Mtls ListT sind kaputt und kommen auch nicht mit nützlichen Funktionen wie takeWhile)


11
2018-06-06 14:18



Sie müssen keine spezielle Art der Ausführung von IO verwenden, Sie müssen nur die Reihenfolge ändern, in der Sie die Dinge tun. Anstatt alle Dateien zu öffnen und dann den Inhalt zu verarbeiten, öffnen Sie eine Datei und drucken jeweils eine Ausgabezeile.

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

BTW, ich benutze zufällig eine andere MD5-Hash-Bibliothek, der Unterschied ist nicht signifikant.

Die Hauptsache, die hier abläuft, ist die Linie:

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

Es öffnet eine einzelne Datei, es verbraucht den gesamten Inhalt der Datei und druckt eine Ausgabezeile. Es schließt die Datei, weil es den gesamten Inhalt der Datei verbraucht. Zuvor verzögerten Sie den Verbrauch der Datei, die sich verzögerte, als die Datei geschlossen wurde.

Wenn Sie sich nicht ganz sicher sind, ob Sie alle Eingaben konsumieren, aber trotzdem sicherstellen möchten, dass die Datei geschlossen wird, können Sie die Datei verwenden withFile Funktion von System.IO:

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

Das withFile Funktion öffnet die Datei und übergibt das Dateihandle an die Body-Funktion. Es garantiert, dass die Datei geschlossen wird, wenn der Körper zurückkehrt. Dieses "withBlah" -Muster ist sehr häufig bei teuren Ressourcen. Dieses Ressourcenmuster wird direkt von System.Exception.bracket.


27
2018-06-06 14:36



HINWEIS: Ich habe meinen Code leicht geändert, um den Rat in zu reflektieren Duncan Coutts 'Antwort. Selbst nach dieser Bearbeitung ist seine Antwort offensichtlich viel besser als meine und scheint nicht auf die gleiche Weise aus dem Speicher zu gehen.


Hier ist mein kurzer Versuch bei einem Iteratee-basierte Version. Wenn ich es in einem Verzeichnis mit ungefähr 2000 kleinen (30-80K) Dateien laufe, ist es ungefähr 30 mal schneller als deine Version hier und scheint etwas weniger Speicher zu verbrauchen.

Aus irgendeinem Grund scheint es immer noch zu viel Speicherplatz auf sehr großen Dateien zu haben - ich verstehe das nicht wirklich Iterateegut genug, um noch schnell sagen zu können, warum.

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)

Beachten Sie, dass Sie die benötigen iteratee Paket und TomMDs pureMD5. (Und ich entschuldige mich, wenn ich hier etwas Schreckliches getan habe - ich bin ein Anfänger mit diesem Zeug.)


6
2018-06-06 06:25



Edit: Meine Annahme war, dass der Benutzer Tausende von sehr kleinen Dateien geöffnet hat, es stellt sich heraus, dass sie sehr groß sind. Faulheit wird wesentlich sein.

Nun, Sie müssen einen anderen IO-Mechanismus verwenden. Entweder:

  • Strict IO (Verarbeitung der Dateien mit Data.ByteString oder System.IO.Strict
  • oder, Iteratee IO (nur für Experten im Moment).

Ich würde auch dringend empfehlen, 'Entpacken' nicht zu verwenden, da dies den Vorteil der Verwendung von Bytestrings zerstört.

Zum Beispiel können Sie Ihre Lazy IO durch System.IO.Strict ersetzen, was Folgendes ergibt:

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
2018-06-05 19:53



Das Problem ist, dass mapM nicht so faul ist, wie Sie denken - es führt zu einer vollständigen Liste mit einem Element pro Dateipfad. Und die Datei IO, die Sie verwenden ist faul, so erhalten Sie eine Liste mit einer geöffneten Datei pro Dateipfad.

Die einfachste Lösung besteht in diesem Fall darin, die Bewertung des Hash für jeden Dateipfad zu erzwingen. Eine Möglichkeit, das zu tun ist mit Control.Exception.evaluate:

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

Wie andere bereits erwähnt haben, arbeiten wir an einem Ersatz für den aktuellen Ansatz für Lazy IO, der allgemeiner und dennoch einfach ist.


2
2018-06-06 10:16



EDIT: Entschuldigung, dachte das Problem war mit den Dateien, nicht die Lese / Durchsage. Ignoriere das.

Kein Problem, öffne die Datei explizit (openFile), lies den Inhalt (Data.ByteString.Lazy.hGetContents), führe den md5-Hash (let! H = md5-Inhalt) aus und schließe die Datei explizit (hClose).


0
2018-06-06 00:42



unsicherInterleaveIO?

Noch eine andere Lösung, die mir in den Sinn kommt, ist zu verwenden unsafeInterleaveIO von System.IO.Unsafe. Siehe die Antwort von Tomasz Zielonka in Dieser Thread im Haskell Cafe.

Es verschiebt eine Eingabe-Ausgabe-Operation (Öffnen einer Datei), bis sie tatsächlich benötigt wird. So kann vermieden werden, dass alle Dateien gleichzeitig geöffnet werden und stattdessen sequenziell gelesen und verarbeitet werden (öffnen Sie sie langsam).

Jetzt glaube ich, mapM getFileLine öffnet alle Dateien, beginnt aber erst mit dem Lesen putStr . unlines. So schweben viele Thunks mit offenen Dateihandlern herum, das ist das Problem. (Bitte korrigieren Sie mich, wenn ich falsch liege).

Ein Beispiel

EIN modifiziertes Beispiel mit unsafeInterleaveIO läuft jetzt mehrere Minuten lang in einem Verzeichnis mit 100 GB in konstantem Speicherplatz.

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

(Ich habe für pureMD5 Implementierung des Hash geändert)

P.S. Ich bin mir nicht sicher, ob das ein guter Stil ist. Ich glaube, dass Lösungen mit Iterees und strengem IO besser sind, aber dieses ist schneller zu machen. Ich benutze es in kleinen Skripten, aber ich hätte Angst davor, mich in einem größeren Programm darauf zu verlassen.


0
2018-06-06 14:06