[英]How to add a finalizer on a TVar
背景
为了回答一个问题 ,我构建并上传了bound-tchan (我不适合上传jnb的版本 )。 如果名称还不够,那么bounded-tchan(BTChan)是具有最大容量的STM通道(如果通道已满,则写入块)。
最近,我收到了添加像常规TChan一样的dup功能的请求。 从而开始了问题。
BTChan的外观
下面是BTChan的简化视图(实际上是无效的)。
data BTChan a = BTChan
{ max :: Int
, count :: TVar Int
, channel :: TVar [(Int, a)]
, nrDups :: TVar Int
}
每次向通道写入内容时,都在元组中包含dups( nrDups
)的数量-这是一个“单个元素计数器”,表示有多少读者获得了该元素。
每个读取器都会减少其读取元素的计数器,然后将其读取指针移至列表中的下一个元素。 如果读取器将计数器递减为零,则递减count
以正确反映通道上的可用容量。
要在期望的语义上明确:通道容量表示在通道中排队的最大元素数。 任何给定元素都会排队,直到每个dup的阅读器收到该元素为止。 任何元素都不应排队等待GCed重复(这是主要问题)。
例如,假设容量为2的通道(c1,c2,c3)有3个复制段,其中2个项被写入通道,然后从c1
和c2
中读出所有项。 通道仍已满 (剩余容量为0),因为c3
尚未消耗其副本。 在任何时间点,如果所有对c3
引用都被删除(因此c3
被GC了),则应释放容量(在这种情况下恢复为2)。
这是问题所在:假设我有以下代码
c <- newBTChan 1
_ <- dupBTChan c -- This represents what would probably be a pathological bug or terminated reader
writeBTChan c "hello"
_ <- readBTChan c
使BTChan看起来像:
BTChan 1 (TVar 0) (TVar []) (TVar 1) --> -- newBTChan
BTChan 1 (TVar 0) (TVar []) (TVar 2) --> -- dupBTChan
BTChan 1 (TVar 1) (TVar [(2, "hello")]) (TVar 2) --> -- readBTChan c
BTChan 1 (TVar 1) (TVar [(1, "hello")]) (TVar 2) -- OH NO!
请注意,最后"hello"
的读取计数仍为1
? 这意味着该消息不会被认为已经消失(即使它在实际实现中会被垃圾回收),并且我们的count
永远不会减少。 由于通道处于最大容量(最多1个元素),因此写入器将始终处于阻塞状态。
我希望每次调用dupBTChan
都创建一个dupBTChan
器。 当收集了一个已钝化(或原始)的通道时,该通道上所有剩余要读取的元素都将使每个元素的计数减少,并且nrDups
变量也将减少。 结果,将来的写入将具有正确的count
(该count
不会为GCed通道未读取的变量保留空间)。
解决方案1-手动资源管理(我要避免的事情)
因此,JNB的bound-tchan实际上具有手动资源管理。 请参见cancelBTChan
。 我要为用户提供更难犯错的东西(不是在很多情况下手动管理不是正确的方法)。
解决方案2-通过阻止TVar来使用异常(GHC无法按照我的意愿执行此操作)
编辑此解决方案,而仅是附带的解决方案3不起作用! 由于存在错误5055 (WONTFIX),GHC编译器会将异常发送到两个阻塞的线程,即使一个线程就足够了(理论上是可以确定的,但对于GHC GC来说并不实际)。
如果获取BTChan
所有方法都是IO,则我们可以forkIO
一个线程,该线程在给定BTChan
唯一的额外(虚拟)TVar字段上读取/重试。 当所有其他对TVar的引用都被删除时,新线程将捕获异常,因此它将知道何时减少nrDups
和单个元素计数器。 这应该可以工作,但会强制所有用户使用IO来获取其BTChan
:
data BTChan = BTChan { ... as before ..., dummyTV :: TVar () }
dupBTChan :: BTChan a -> IO (BTChan a)
dupBTChan c = do
... as before ...
d <- newTVarIO ()
let chan = BTChan ... d
forkIO $ watchChan chan
return chan
watchBTChan :: BTChan a -> IO ()
watchBTChan b = do
catch (atomically (readTVar (dummyTV b) >> retry)) $ \e -> do
case fromException e of
BlockedIndefinitelyOnSTM -> atomically $ do -- the BTChan must have gotten collected
ls <- readTVar (channel b)
writeTVar (channel b) (map (\(a,b) -> (a-1,b)) ls)
readTVar (nrDup b) >>= writeTVar (nrDup b) . (-1)
_ -> watchBTChan b
编辑:是的,这是一个穷人的终结器,我没有任何特殊的理由要避免使用addFinalizer
。 那将是相同的解决方案,仍然迫使使用IO afaict。
解决方案3:比解决方案2更干净的API,但是GHC仍然不支持它
用户通过调用initBTChanCollector
启动管理器线程,该线程将监视一组这些虚拟TVar(来自解决方案2)并进行所需的清理。 基本上,它将IO推到另一个线程中,该线程知道通过全局( unsafePerformIO
ed) TVar
做什么。 事情基本上像解决方案2一样工作,但是BTChan的创建仍然可以是STM。 运行initBTChanCollector
失败会导致进程运行时任务列表(空间泄漏)不断增加。
解决方案4:禁止丢弃BTChan
这类似于忽略该问题。 如果用户从不丢弃重复的BTChan
则问题将消失。
解决方案5我看到了ezyang的答案(完全有效并受到赞赏),但实际上我想仅使用“ dup”功能保留当前的API。
**解决方案6 **请告诉我还有更好的选择。
编辑:我实现了解决方案3 (完全未经测试的alpha版本),并通过使全局自身成为BTChan
来处理了潜在的空间泄漏-该chan的容量应该为1,所以忘记运行init
确实显示得很快,但这只是一个小小的变化。 这在GHCi(7.0.3)中有效,但这似乎是偶然的。 GHC对两个被阻塞的线程(读取BTChan和监视线程的有效线程)都抛出异常,因此,如果另一个线程丢弃它的引用时被阻塞读取BTChan,那我就死了。
这是另一种解决方案:要求对有界通道重复项的所有访问都由一个函数括起来,该函数在退出时释放其资源(通过异常或通常)。 您可以将Monad与2级赛跑者一起使用,以防止重复的频道泄漏出去。 它仍然是手动的,但是类型系统使调皮的事情变得更加困难。
您真的不想依赖真正的IO终结器,因为GHC无法保证何时可以运行终结器:就您所知,它可能要等到程序结束后才能运行终结器,这意味着您陷入了僵局。直到那时。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.