haskell - Asesinando un hilo cuando MVar es basura
concurrency garbage-collection (4)
Aunque BlockedIndefinitelyOnMVar
debería funcionar, considere también el uso de finalizadores de ForeignPointer . El rol normal de estos es eliminar estructuras C que ya no son accesibles en Haskell. Sin embargo, puede adjuntar cualquier finalizador IO.
Tengo un hilo de trabajo que lee datos repetidamente de un MVar y realiza un trabajo útil sobre eso. Después de un tiempo, el resto del programa se olvida de ese hilo de trabajo, lo que significa que esperará en un MVar vacío y se volverá muy solitario. Mi pregunta es:
¿Será el MVar recogido basura si los hilos ya no le escriben, por ejemplo porque todos lo esperan? ¿La recolección de basura matará a los hilos de espera? Si ninguno de los dos, ¿puedo indicar de alguna manera al compilador que el MVar debería ser recolectado como basura y que se debe eliminar el hilo?
EDITAR: probablemente debería aclarar el propósito de mi pregunta. No deseo protección general contra interbloqueos; en cambio, lo que me gustaría hacer es vincular la vida del hilo de trabajo a la vida de un valor (como en: los valores muertos son reclamados por la recolección de basura). En otras palabras, el hilo de trabajo es un recurso que me gustaría liberar no a mano, sino cuando un cierto valor (el MVar o un derivado) es basura.
Aquí un ejemplo de programa que demuestra lo que tengo en mente
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"
En otras palabras, quiero que se elimine el hilo cuando ya no puedo comunicarme con él, es decir, cuando el MVar utilizado para la comunicación ya no está dentro del alcance. ¿Como hacer eso?
Probé el simple MVar débil y se finalizó y mató. El código es:
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 "*"
La sesión con el programa fue:
(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
*
Así que el bloqueo en takeMVar no mantuvo vivo al MVar en ghc-7.4.1 a pesar de las expectativas.
Si tiene suerte, recibirá un "BlockedIndefinitelyOnMVar" , que indica que está esperando un MVar que ningún hilo escribirá nunca.
Pero, para citar a Ed Yang,
GHC solo sabe que un hilo puede considerarse basura si no hay referencias al hilo. ¿Quién está sosteniendo una referencia al hilo? El MVar, ya que el hilo está bloqueando en esta estructura de datos y se ha agregado a la lista de bloqueo de esto. ¿Quién mantiene vivo al MVar? Por qué, nuestro cierre que contiene un llamado para tomarMVar. Entonces el hilo se queda.
sin un poco de trabajo (que, por cierto, sería bastante interesante de ver), BlockedIndefinitelyOnMVar no es un mecanismo obviamente útil para proteger a los programas Haskell de interbloqueos.
GHC simplemente no puede resolver el problema en general de saber si su hilo progresará.
Un mejor enfoque sería terminar explícitamente los hilos al enviarles un mensaje Done
. Por ejemplo, simplemente eleve su tipo de mensaje a un valor opcional que también incluya un valor de fin de mensaje:
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
y obtenemos la limpieza correcta:
$ ./A
"Haskell"
"42"
"Done!"
"Still here"
Simplemente funcionará: cuando el MVar
solo es alcanzable por el hilo que está bloqueado en él, el hilo recibe la excepción BlockedIndefinitelyOnMVar
, que normalmente provocará que muera silenciosamente (el controlador de excepciones predeterminado para un hilo ignora esta excepción).
Por cierto, para hacer algo de limpieza cuando el hilo muere, querrás usar forkFinally
(que acabo de agregar a Control.Concurrent
).