我编写了一个名为amqp-worker的库,它提供了一个名为worker
的函数,它为消息轮询消息队列(如RabbitMQ),在发现消息时调用处理程序。然后它回到投票。
它正在泄漏记忆。我已对它进行了分析,图表显示PAP
(部分功能应用程序)是罪魁祸首。 我的代码泄漏在哪里?使用IO
forever
进行循环时如何避免泄漏?
以下是一些相关功能。 The full source is here
main :: IO ()
main = do
-- connect
conn <- Worker.connect (fromURI "amqp://guest:guest@localhost:5672")
-- initialize the queues
Worker.initQueue conn queue
Worker.initQueue conn results
-- publish a message
Worker.publish conn queue (TestMessage "hello world")
-- create a worker, the program loops here
Worker.worker def conn queue onError (onMessage conn)
worker :: (FromJSON a, MonadBaseControl IO m, MonadCatch m) => WorkerOptions -> Connection -> Queue key a -> (WorkerException SomeException -> m ()) -> (Message a -> m ()) -> m ()
worker opts conn queue onError action =
forever $ do
eres <- consumeNext (pollDelay opts) conn queue
case eres of
Error (ParseError reason bd) ->
onError (MessageParseError bd reason)
Parsed msg ->
catch
(action msg)
(onError . OtherException (body msg))
liftBase $ threadDelay (loopDelay opts)
consumeNext :: (FromJSON msg, MonadBaseControl IO m) => Microseconds -> Connection -> Queue key msg -> m (ConsumeResult msg)
consumeNext pd conn queue =
poll pd $ consume conn queue
poll :: (MonadBaseControl IO m) => Int -> m (Maybe a) -> m a
poll us action = do
ma <- action
case ma of
Just a -> return a
Nothing -> do
liftBase $ threadDelay us
poll us action
答案 0 :(得分:15)
这是一个非常简单的示例,用于演示您的问题:
main :: IO ()
main = worker
{-# NOINLINE worker #-}
worker :: (Monad m) => m ()
worker =
let loop = poll >> loop
in loop
poll :: (Monad m) => m a
poll = return () >> poll
If you remove the `NOINLINE`, or specialize `m` to
`IO` (while compiling with `-O`), the leak goes away.
我写了一篇详细的blog
post来解释原因
这个代码正好泄漏了内存。快速总结是,正如里德指出的那样
回答,代码创建并记住了一系列部分应用程序
>>
秒。
我还提交了一份关于此问题的ghc ticket。
答案 1 :(得分:4)
也许一个更容易理解的例子就是这个
main :: IO ()
main = let c = count 0
in c >> c
{-# NOINLINE count #-}
count :: Monad m => Int -> m ()
count 1000000 = return ()
count n = return () >> count (n+1)
评估f >> g
的IO操作会产生某种类型的闭包,它引用f
和g
(它基本上是f
和g
的组合作为状态令牌的功能)。 count 0
会返回一个thunk c
,它将评估return () >> return () >> return () >> ...
形式的大型闭包结构。当我们执行c
时,我们构建了这个结构,因为我们必须第二次执行c
整个结构仍然存在。所以这个程序会泄漏内存(无论优化标志如何)。
当count
专门用于IO
并且启用了优化时,GHC可以使用各种技巧来避免构建此数据结构;但他们都依赖于知道monad是IO
。
回到原来的count :: Monad m => Int -> m ()
,我们可以尝试通过将最后一行更改为
count n = return () >>= (\_ -> count (n+1))
现在递归调用隐藏在lambda中,因此c
只是一个小结构return () >>= (\_ -> BODY)
。这实际上避免了在没有优化的情况下进行编译时的空间泄漏。但是,当启用优化时,GHC从lambda的主体中浮出count (n+1)
(因为它不依赖于参数)生成
count n = return () >>= (let body = count (n+1) in \_ -> body)
现在c
又是一个大型结构......
答案 2 :(得分:3)
内存泄漏位于CREATE OR REPLACE VIEW your_view AS
SELECT item,
step_1,
step_2,
step_3,
step_4,
step_5,
( CASE WHEN step_1 IN ('OK', 'N/A') THEN 1 ELSE 0 END
+ CASE WHEN step_2 IN ('OK', 'N/A') THEN 1 ELSE 0 END
+ CASE WHEN step_3 IN ('OK', 'N/A') THEN 1 ELSE 0 END
+ CASE WHEN step_4 IN ('OK', 'N/A') THEN 1 ELSE 0 END
+ CASE WHEN step_5 IN ('OK', 'N/A') THEN 1 ELSE 0 END)
/ 5
* 100
|| '%'
AS progress
FROM your_table
。使用monad-loops
,我将定义更改为以下内容:step
看起来像我的递归一样,但修复了泄漏。
有人可以评论为什么我之前对poll
的定义是在泄漏记忆吗?
untilJust