[英]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.