导管:导致内存泄漏

3

我在对之前的一个问题 (haskell-data-hashset-from-unordered-container-performance-for-large-sets) 进行观察时,发现了一个奇怪的内存泄漏。

module Main where

import System.Environment (getArgs)
import Control.Monad.Trans.Resource (runResourceT)
import Data.Attoparsec.ByteString (sepBy, Parser)
import Data.Attoparsec.ByteString.Char8 (decimal, char)
import Data.Conduit
import qualified Data.Conduit.Attoparsec as CA
import qualified Data.Conduit.Binary as CB
import qualified Data.Conduit.List as CL

main :: IO ()
main = do (args:_) <- getArgs
          writeFile "input.txt" $ unlines $ map show [1..4 :: Int]
          case args of "list" -> m1
                       "fail" -> m2
                       "listlist" -> m3
                       "memoryleak" -> m4
                       --UPDATE
                       "bs-lines":_ -> m5
                       "bs":_ -> m6
                       _ -> putStr $ unlines ["Usage: conduit list"
                                             ,"               fail"
                                             ,"               listlist"
                                             ,"               memoryleak"
                                             --UPDATE
                                             ,"               bs-lines"
                                             ,"               bs"
                                             ]
m1,m2,m3,m4 :: IO ()
m1 = do hs <- runResourceT
            $  CB.sourceFile "input.txt"
            $$ CB.lines
           =$= CA.conduitParser (decimal :: Parser Int)
           =$= CL.map snd
           =$= CL.consume
        print hs
m2 = do hs <- runResourceT
            $  CB.sourceFile "input.txt"
            $$ CA.conduitParser (decimal :: Parser Int)
           =$= CL.map snd
           =$= CL.consume
        print hs
m3 = do hs <- runResourceT
            $  CB.sourceFile "input.txt"
            $$ CB.lines
           =$= CA.conduitParser (decimal `sepBy` (char '\n') :: Parser [Int])
           =$= CL.map snd
           =$= CL.consume
        print hs
m4 = do hs <- runResourceT
            $  CB.sourceFile "input.txt"
            $$ CA.conduitParser (decimal `sepBy` (char '\n') :: Parser [Int])
           =$= CL.map snd
           =$= CL.consume
        print hs
-- UPDATE
m5 = do inpt <- BS.lines <$> BS.readFile "input.txt"
        let Right hs =  mapM (parseOnly (decimal :: Parser Int)) inpt
        print hs
m6 = do inpt <- BS.readFile "input.txt"
        let Right hs =  (parseOnly (decimal `sepBy` (char '\n') :: Parser [Int])) inpt
        print hs

以下是一些示例输出:
$ > stack exec -- example list
[1234]
$ > stack exec -- example listlist
[[1234]]
$ > stack exec -- conduit fail
conduit: ParseError {errorContexts = [], errorMessage = "Failed reading: takeWhile1", errorPosition = 1:2}
$ > stack exec -- example memoryleak
(Ctrl+C)

-- UPDATE
$ > stack exec -- example bs-lines
[1,2,3,4]
$ > stack exec -- example bs
[1,2,3,4]

我现在有几个问题:

  • 为什么 m1 没有产生 [1,2,3,4]
  • 为什么 m2 失败了?
  • 为什么 m4 与所有其他版本的行为完全不同,并产生空间泄漏?

你是否仅使用lazy I/O和attoparsec本身检查了代码?如果attoparsec中的回溯比预期的要长,我不会感到惊讶。 - Michael Snoyman
@MichaelSnoyman 我更新了我的问题,使用了bytestring/attoparsec版本,但我不确定它是否等同于conduit版本(因为我对conduit不太熟悉)。至少它产生了我期望的输出。 - epsilonhalbe
2个回答

3
为什么m2会失败?
输入文件作为字符流是:
1\n2\n3\n4\n

由于decimal解析器不期望换行符,所以消耗第一个数字后,剩余的流为:

\n2\n3\n4\n

由于输入流并未耗尽,conduitParser将再次对流运行解析器,但这一次它甚至无法消耗第一个字符,因此失败了。

为什么m4的行为与所有其他版本完全不同,并且会产生空间泄漏?

decimal `sepBy` (char '\n') 只会在两个整数之间消耗\n。成功解析四个数字后,输入流中只剩下一个字符。
\n

decimal`sepBy`(char '\n')无法解析它,更糟的是它不会失败:sepBy可以什么也不解析并返回空列表。因此,它会无限制地解析什么也没有,并永远不会终止。

为什么m1不能产生[1,2,3,4]?

我也想知道!我猜这可能与融合有关,也许你应该联系conduit软件包的作者,他刚刚评论了你的问题。


2
由于解释太长无法以评论形式添加,我将其作为另一个答案添加。 - Michael Snoyman

2
回答关于m1的问题:当您使用CB.lines时,您将输入转换为以下形式:
["1\n2\n3\n4\n"]

into:

["1", "2", "3", "4"]

然后,attoparsec解析“1”,等待更多输入,看到“2”,以此类推。

那么使用 conduitParser 是不正确的吗?我应该使用普通的 parseOnlyCL.map 吗?如果我理解正确,conduitParser 逐步解析流并不适合解析 [1,2,3,4] - epsilonhalbe
这不是我想说的。结合CB.linesconduitParser是有问题的,因为前者会剥离出后者需要进行正确解析的换行符。 - Michael Snoyman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接