2011-03-27 10 views
5

Antecedentescómo añadir un finalizador en un TVAR

En respuesta a una question, he construido y uploaded a bounded-tchan (no habría sido adecuado para mí para cargar jnb's version). Si el nombre no es suficiente, un bounded-tchan (BTChan) es un canal STM que tiene una capacidad máxima (escribe el bloque si el canal está en su capacidad).

Recientemente, recibí una solicitud para agregar una función de duplicado como en el regular TChan's. Y así comienza el problema.

cómo se ve el BTChan

una vista simplificada (y de hecho no funcional) de BTChan está por debajo.

data BTChan a = BTChan 
    { max :: Int 
    , count :: TVar Int 
    , channel :: TVar [(Int, a)] 
    , nrDups :: TVar Int 
    } 

Cada vez que se escribe en el canal de incluir el número de DUP (nrDups) en la tupla - esto es un 'contador de elementos individuales' que indica cómo muchos lectores han llegado a este elemento.

Cada lector disminuirá el contador para el elemento que lee y luego moverá su punto de lectura al siguiente elemento de la lista. Si el lector reduce el contador a cero, entonces el valor de count se reduce para reflejar adecuadamente la capacidad disponible en el canal.

Para tener claridad sobre la semántica deseada: Una capacidad de canal indica la cantidad máxima de elementos en cola en el canal. Cualquier elemento dado se pone en cola hasta que un lector de cada dup haya recibido el elemento. Ningún elemento debe permanecer en cola para un duplicado GCed (este es el problema principal).

Por ejemplo, que haya tres dups de un canal (c1, c2, c3) con capacidad de 2, donde se escribieron 2 elementos en el canal y luego se leyeron c1 y c2. El canal es todavía lleno (0 capacidad restante) porque c3 no ha consumido sus copias. En cualquier punto en el tiempo si todas las referencias a c3 se descartan (por lo que c3 se GCed), entonces la capacidad debe liberarse (restaurada a 2 en este caso).

Aquí está el problema: vamos a decir que tengo el siguiente código

c <- newBTChan 1 
_ <- dupBTChan c -- This represents what would probably be a pathological bug or terminated reader 
writeBTChan c "hello" 
_ <- readBTChan c 

Causando la BTChan para que parezca:

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! 

Aviso al final del lea contar para "hello" es todavía 1 ? Eso significa que el mensaje no se considera ido (a pesar de que recibirá GCed en la implementación real) y nuestro count nunca disminuirá. Debido a que el canal está en capacidad (1 elemento máximo), los escritores siempre bloquearán.

Quiero un finalizador creado cada vez que se llama dupBTChan. Cuando se recolecta un canal duplicado (u original), todos los elementos que quedan por leer en ese canal obtendrán un decremento del conteo por elemento, también se disminuirá la variable nrDups. Como resultado, las escrituras futuras tendrán el count correcto (un count que no reserva espacio para variables no leídas por los canales GCed).

Solución 1 - Manual de Gestión de Recursos (lo que yo quiero evitar)

delimitada-Tchan de JNB tiene realmente la gestión manual de recursos por este motivo. Vea el cancelBTChan. Voy a buscar algo más difícil para el usuario equivocarse (no es que la gestión manual no sea la correcta en muchos casos).

Solución 2 - Utilice excepciones mediante el bloqueo de TVars (GHC no puede hacerlo como yo quiero)

editar esta solución, y la solución 3, que es sólo un spin-off, no funciona! Debido a bug 5055 (WONTFIX), el compilador de GHC envía excepciones a ambos hilos bloqueados, aunque uno es suficiente (que es teóricamente determinable, pero no es práctico con el GC de GHC).

Si todas las formas de obtener una BTChan son IO, podemos forkIO un hilo que lee/reintentos en un (ficticio) TVAR campo adicional única de la BTChan dada. El nuevo hilo atrapará una excepción cuando se eliminen todas las demás referencias al TVar, por lo que sabrá cuándo decrementar el nrDups y los contadores de elementos individuales. Esto debería funcionar, pero las fuerzas de todos mis usuarios para utilizar la IO para obtener sus BTChan s:

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 

EDIT: Sí, esto es un mal finalizador Mans y no tengo ninguna razón particular para evitar el uso de addFinalizer. Esa sería la misma solución, aún forzando el uso de IO afaict.

Solución 3: Una API más limpio que la solución 2, pero GHC todavía no lo soporta

usuarios iniciar un hilo gerente llamando initBTChanCollector, que hará un seguimiento de un conjunto de estos TVars ficticias (de la solución 2) y hacer la limpieza necesaria. Básicamente, coloca el IO en otro hilo que sabe qué hacer a través de un archivo global (unsafePerformIO ed) TVar. Las cosas funcionan básicamente como la solución 2, pero la creación de BTChan todavía puede ser STM. Si no se ejecuta initBTChanCollector, se produciría una lista en constante crecimiento (fuga de espacio) de tareas a medida que se ejecuta el proceso.

Solución 4: Nunca permita descartar BTChan s

Esto es similar a ignorar el problema. Si el usuario nunca tira un BTChan duplicado, el problema desaparece.

Solución 5 veo la respuesta de ezyang (totalmente válido y apreciado), pero en realidad le gustaría mantener la API actual acaba con una función 'dup'.

** Solución 6 ** Por favor, dime que hay una mejor opción.

EDIT: I implemented solution 3 (versión alfa totalmente no probado) y manipulado el potencial de fugas espacio, haciendo que el global en sí mismo un BTChan - que Chan probablemente debería tener una capacidad de 1 por lo que olvidarse de ejecutar init muestra hasta muy rápido, pero eso es un cambio menor Esto funciona en GHCi (7.0.3) pero parece ser incidental. GHC arroja excepciones a ambos hilos bloqueados (el válido que lee el BTChan y el hilo de observación) por lo que si estás bloqueado leyendo un BTChan cuando otro hilo descarta su referencia, entonces mueres.

+0

No entiendo exactamente qué tienes en mente. ¿Cuál debería ser la semántica de los canales duplicados con respecto a los recursos? ¿Un canal bloquea si tanto él como el duplicado están llenos? Si uno de ellos está lleno? –

+0

Correcto, esta semántica debe ser aclarada. Si intentas implementar "Un bloque de canales si tanto el como el duplicado están llenos", entonces debes preguntar: ¿puedo dejar elementos de la cola? Si la respuesta es no, entonces tienes un canal ilimitado nuevamente. –

+0

(También sería útil preguntar a la persona que solicitó el duplicado para qué lo estaban utilizando). –

Respuesta

5

Aquí hay otra solución: requiere que todos los accesos al duplicado del canal delimitado estén entre corchetes mediante una función que libera sus recursos al salir (por una excepción o normalmente). Puede usar una mónada con un corredor de rango 2 para evitar que los canales duplicados se filtren. Todavía es manual, pero el sistema de tipo hace que sea mucho más difícil hacer cosas malas.

Realmente no desea confiar en los finalizadores IO verdaderos, porque GHC no ofrece garantías sobre cuándo se puede ejecutar un finalizador: por lo que sabe, puede esperar hasta el final del programa antes de ejecutar el finalizador, lo que significa estás estancado hasta entonces.

+2

Después de haber pensado en esto por un tiempo, estoy de acuerdo. Simplemente es incorrecto intentar usar los finalizadores para cualquier otra cosa que no sea gestión de memoria, es decir. efectos que no son semánticamente "observables" desde dentro de Haskell. El equivalente idiomático de Haskell de RAII no es finalizadores, sino funciones 'con'. – sclv

Cuestiones relacionadas