簡體   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