为什么一个指向MVar的弱引用会被终止,即使MVar仍然可以访问?

3
我发现在使用MVar和Weak的时候存在问题:即使MVar仍然可以从函数作用域中访问(并且主线程正在阻塞它),Weak认为它应该被终止。只有启用优化编译时,这种行为才会发生。
平台:64位Linux
已验证GHC版本:6.10.4、6.12.3、7.0.4、7.2.2、7.4.1
module Main (main) where

import           Control.Concurrent
import           Control.Monad (forever, forM_)
import           Data.IORef
import           System.Mem
import           System.Mem.Weak

dispatchPendingCalls :: IORef [Weak (MVar ())] -> IO ()
dispatchPendingCalls ref = forever $ do
    threadDelay 100000

    pending <- atomicModifyIORef ref (\p -> ([], p))
    forM_ pending (\weak -> do
        maybeMVar <- deRefWeak weak
        case maybeMVar of
            Just mvar -> putMVar mvar ()
            Nothing -> putStrLn "dispatchPendingCalls: weak mvar is Nothing")

call :: IORef [Weak (MVar ())] -> IO ()
call ref = do
    mvar <- newEmptyMVar
    weak <- mkWeakPtr mvar (Just (putStrLn "call: finalising weak"))

    putStrLn "call: about to insert weak into list"
    atomicModifyIORef ref (\p -> (weak : p, ()))
    putStrLn "call: inserted weak into list"
    performGC
    takeMVar mvar
    putStrLn "call: took from mvar"

main :: IO ()
main = do
    pendingCalls <- newIORef []
    _ <- forkIO (dispatchPendingCalls pendingCalls)
    call pendingCalls

期望输出:

$ ghc --make WeakMvar.hs
$ ./WeakMvar
call: about to insert weak into list
call: inserted weak into list
call: took from mvar
$

实际输出:

$ ghc --make -O2 WeakMvar.hs
$ ./WeakMvar
call: about to insert weak into list
call: inserted weak into list
call: finalizing weak
dispatchPendingCalls: weak mvar is Nothing
(never exits)

为什么会出现这种情况?如果我正确理解了System.Mem.Weak文档,那么takeMVar mvar行应该保持mvar的活动状态,从而使其弱指针有效。然而,弱指针却认为在调用takeMVar返回之前,MVar已经变得不可访问。

FYI:他提交了一个错误报告http://hackage.haskell.org/trac/ghc/ticket/6130 根据此,应在GHC 7.6.1中修复。 - Chetan
2个回答

1
这几乎可以确定是因为 GHC 的优化倾向于移除附加了终结器的数据结构,导致终结器运行过早。也就是说,终结器引用了 MVar 数据构造函数而不是底层的 MVar#当前文档中有一些关于此的警告。如果我使用 Control.Concurrent.MVar.mkWeakMVar,我会看到预期的输出(使用 ghc-7.6.3)。

1

尝试在takeMVar中的call中捕获BlockedIndefinitelyOnMVar(默认情况下由IIR处理,因此您看不到它)。我猜使用Weak可以防止GC注意到您对dispatchPendingCallsMVar的引用,因此它被垃圾回收了?


使用 catch (takeMVar mvar) (\BlockedIndefinitelyOnMVar -> putStrLn "blocked") 不会改变行为。此外,GC 不应该回收 mvar,因为 takeMVar 应该保持其存活状态。 - John Millikin
不确定发生了什么,但是当所有其他引用消失或也被阻塞时,运行时肯定会对使用takeMVar在MVar上无限期阻塞的线程进行垃圾回收。 - jberryman

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