当MVar被垃圾回收时如何终止一个线程

34

我有一个工作线程反复从MVar读取数据并对其进行一些有用的工作。一段时间后,程序的其他部分会忘记这个工作线程,这意味着它将等待空的MVar并变得孤单。我的问题是:

如果线程不再向MVar写入数据(例如因为它们都在等待它),那么MVar是否会被垃圾收集? 垃圾回收是否会杀死正在等待的线程? 如果都不是,我能否以某种方式指示编译器应该回收MVar并杀死该线程?

编辑: 我应该澄清我的问题的目的。我不需要一般防止死锁的保护;相反,我想要做的是将工作线程的生命周期绑定到一个值的生命周期(例如:垃圾收集会清理不再使用的值)。换句话说,工作线程是一种资源,我希望在某个值(MVar或导出的值)被垃圾收集时自动释放。


这里是一个演示我想法的示例程序:

import Control.Concurrent
import Control.Concurrent.MVar

main = do
    something
    -- the thread forked in  something  can  be killed here
    -- because the  MVar  used for communication is no longer in scope
    etc

something = do
    v <- newEmptyMVar
    forkIO $ forever $ work =<< takeMVar v
    putMVar v "Haskell"
    putMVar v "42"
换句话说,我希望当我无法再与MVar进行通信时(即MVar不再在作用域内),线程能够被终止。如何实现?

1
在这里使用弱引用到MVar是否可行? - Chris Kuklewicz
@JohnL:确实如此,但如果线程限制等待MVar的时间,并定期检查弱引用是否仍然存在呢?(弱引用可能必须指向另一个值,与MVar同时超出范围。)理想情况下,周期应由垃圾收集的频率给出。 - Heinrich Apfelmus
@HeinrichApfelmus:我的第一个编辑基本上以一种方式实现了这一点。你提出了另一种可能更好的方法:在阻塞线程中产生异步异常并引发另一个线程,从弱指针重新读取来中断读取并强制重新读取。我将尝试在今天晚些时候编写一些代码。但是,仍然认为这似乎是很多工作,使用该语言的易于出错的部分解决可以轻松使用dons技术解决的问题。 - John L
@JohnL:我在ghc7.4.1上测试了弱MVar,它可以正常工作,而不需要异步超时。以下是代码。 - Chris Kuklewicz
@ChrisKuklewicz 偶尔会出现一些我主要传播错误信息的帖子。这是其中之一。我已经删除了我所写的大部分内容;我认为我正确的唯一部分是,根据弱引用文档,你的代码应该可以正常工作(就像它现在做的那样)。现在闭嘴... - John L
@JohnL:你对于使用'addMVarFinalizer'的观点并没有错,这种关联会防止垃圾回收。我下面发现的'mkWeakPtr'技巧主要是偶然发现的——直到后来我才明白它为什么有效。 - Chris Kuklewicz
4个回答

27

它只会起作用:当MVar仅被阻塞在其上的线程可达时,该线程将收到BlockedIndefinitelyOnMVar异常,通常会导致其静默死亡(线程的默认异常处理程序会忽略此异常)。

顺便说一下,如果要在线程死亡时进行一些清理工作,您需要使用forkFinally(我刚刚添加Control.Concurrent中)。


太好了!我可以在线程中仍然捕获BlockedIndefinitelyOnMVar异常,对吗? - Heinrich Apfelmus
当然,BlockedIndefinitelyOnMVar 只是一个普通的异常。 - Simon Marlow
2
在多个线程中可以访问MVar的情况下,如果它们都被阻塞在它上面,这是真的吗?此外,从垃圾回收的角度来看,当TSO被阻塞在MVar上时,它是否会从活动集合中移除,并且只能通过MVar访问? - John L

22
如果你很幸运,你会得到一个"BlockedIndefinitelyOnMVar",表示你正在等待一个永远不会被任何线程写入的MVar。
但是,引用Ed Yang的话:
GHC只知道如果没有对线程的引用,线程可以被视为垃圾。谁持有对线程的引用?MVar,因为线程正在阻塞在这个数据结构上,并将自己添加到此列表的阻塞列表中。谁保持MVar的活动状态?我们包含调用takeMVar的闭包。所以线程留下了。
如果没有一些工作(顺便说一下,这将是非常有趣的),BlockedIndefinitelyOnMVar对于提供Haskell程序死锁保护的机制并不明显有用。
GHC通常不能解决问题,即无法确定线程是否会取得进展。
更好的方法是通过向线程发送“完成”消息来显式终止线程。例如,只需将您的消息类型提升为可选值,该值还包括结束消息值:
import Control.Concurrent
import Control.Concurrent.MVar
import Control.Monad
import Control.Exception
import Prelude hiding (catch)

main = do
    something

    threadDelay (10 * 10^6)
    print "Still here"

something = do
    v <- newEmptyMVar
    forkIO $
        finally
            (let go = do x <- takeMVar v
                         case x of
                            Nothing -> return ()
                            Just v  -> print v >> go
             in go)
            (print "Done!")

    putMVar v $ Just "Haskell"
    putMVar v $ Just "42"

    putMVar v Nothing

然后我们得到了正确的清理结果:

$ ./A
"Haskell"
"42"
"Done!"
"Still here"

1
啊,我明白了,所以我必须手动杀死线程(我也可以向它抛出异常)。不过我应该澄清一下我真正想做的事情。也就是说,我想将线程的生命周期与值的生命周期绑定在一起,即将线程视为一个资源,在MVar的垃圾回收时释放,就像readFile的结果的垃圾回收应该关闭文件一样。 - Heinrich Apfelmus

11

我测试了简单的弱MVar,它确实被终止和删除了。代码如下:

import Control.Monad
import Control.Exception
import Control.Concurrent
import Control.Concurrent.MVar
import System.Mem(performGC)
import System.Mem.Weak

dologger :: MVar String -> IO ()
dologger mv = do
  tid <- myThreadId
  weak <- mkWeakPtr mv (Just (putStrLn "X" >> killThread tid))
  logger weak

logger :: Weak (MVar String) -> IO ()
logger weak = act where
  act = do
    v <- deRefWeak weak
    case v of
      Just mv -> do
       a <- try (takeMVar mv) :: IO (Either SomeException String)
       print a
       either (\_ -> return ()) (\_ -> act) a
      Nothing -> return ()

play mv = act where
  act = do
    c <- getLine
    if c=="quit" then return ()
       else putMVar mv c >> act

doplay mv = do
  forkIO (dologger mv)
  play mv

main = do
  putStrLn "Enter a string to escape, or quit to exit"
  mv <- newEmptyMVar
  doplay mv

  putStrLn "*"
  performGC
  putStrLn "*"
  yield
  putStrLn "*"
  threadDelay (10^6)
  putStrLn "*"

该程序的会话如下:

(chrisk)-(/tmp)
(! 624)-> ghc -threaded -rtsopts --make weak2.hs 
[1 of 1] Compiling Main             ( weak2.hs, weak2.o )
Linking weak2 ...

(chrisk)-(/tmp)
(! 625)-> ./weak2 +RTS -N4 -RTS
Enter a string to escape, or quit to exit
This is a test
Right "This is a test"
Tab Tab
Right "Tab\tTab"
quit
*
*
X
*
Left thread killed
*

因此,尽管预期在ghc-7.4.1上对takeMVar执行阻塞操作并不会保持MVar处于活动状态。


不错!因此,您的代码清楚地证明了在线程仍在等待MVar时运行终结器。似乎takeMVar将解构MVar的内部表示,以至于“外壳”可以被垃圾回收。通过使用data Lazy a = Lazy a并在Lazy (MVar String)上放置终结器而不是直接在MVar String上,可能可以实现任何数据结构的类似效果。 - Heinrich Apfelmus
是的,我也弄清楚了。addMVarFinalizer和addFinalizer之间的区别。放入MVar中的最后一项可能无法检索,但我认为对于这种用途来说这是可以接受的。 - Chris Kuklewicz

1

虽然 BlockedIndefinitelyOnMVar 可以工作,但也考虑使用 ForeignPointer finalizers。它们的正常作用是删除在 Haskell 中不再可访问的 C 结构。但是,您可以将任何 IO 终结器附加到它们上。


如果我没记错的话,你不能将任何IO操作附加到它们上面,必须使用C finalizer。这还是真的吗? - Heinrich Apfelmus
一个指向Haskell函数的指针,使用声明为生成正确类型的FunPtr的包装器存根创建。 [Foreign.Ptr] (http://www.haskell.org/ghc/docs/6.12.2/html/libraries/base-4.2.0.1/Foreign-Ptr.html#t%3AFunPtr) - dmbarbour
这并不容易。http://www.haskell.org/ghc/docs/latest/html/users_guide/ffi-ghc.html - Heinrich Apfelmus

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