繁体   English   中英

如何在TVar上添加终结器

[英]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个项被写入通道,然后从c1c2中读出所有项。 通道仍已满 (剩余容量为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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM