Domanda

Sfondo

In risposta a a domanda, Ho costruito e Caricato un tchan limitato (Non sarebbe stato giusto per me a caricare versione di JNB). Se il nome non è sufficiente, un tchan limitato (BTCHAN) è un canale STM che ha una capacità massima (scrive blocco se il canale è in capacità).

Di recente, ho ricevuto una richiesta per aggiungere una funzione DUP come in Tchan regolari. E quindi inizia il problema.

Come appare il Btchan

Una vista semplificata (e in realtà non funzionale) di Btchan è sotto.

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

Ogni volta che scrivi al canale includi il numero di DUP (nrDups) Nella tupla - questo è un "contatore individuale" che indica quanti lettori hanno ottenuto questo elemento.

Ogni lettore diminuirà il contatore per l'elemento che legge, quindi sposterà il punto di lettura all'elemento successivo nell'elenco. Se il lettore diminuisce il contatore a zero, allora il valore di count viene decrementato per riflettere correttamente la capacità disponibile sul canale.

Per essere chiari sulla semantica desiderata: una capacità del canale indica il numero massimo di elementi in coda nel canale. Ogni dato elemento viene messo in coda fino a quando un lettore di ciascun DUP non ha ricevuto l'elemento. Nessun elemento dovrebbe rimanere in coda per un DUP GCED (questo è il problema principale).

Ad esempio, lascia che ci siano tre dup di un canale (C1, C2, C3) con capacità di 2, in cui 2 articoli sono stati scritti nel canale, quindi tutti gli articoli sono stati letti fuori c1 e c2. Il canale è Ancora pieno (0 capacità rimanente) perché c3 non ha consumato le sue copie. In qualsiasi momento se tutti i riferimentic3 sono lasciati cadere (quindi c3 è gced) quindi la capacità dovrebbe essere liberata (ripristinata a 2 in questo caso).

Ecco il problema: Diciamo che ho il seguente codice

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

Causando assomigliare al 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!

Avviso alla fine il conteggio delle letture per "hello" è ancora 1? Ciò significa che il messaggio non è considerato sparito (anche se verrà messo a segno nella vera implementazione) e il nostro count non diminuirà mai. Poiché il canale è a capacità (massimo di 1 elemento), gli scrittori bloccheranno sempre.

Voglio un finalizer creato ogni volta dupBTChan è chiamato. Quando viene raccolto un canale dupped (o originale), tutti gli elementi rimanenti da leggere su quel canale otterranno il conteggio degli elementi decrementati, anche il nrDups la variabile verrà decrementato. Di conseguenza, le scritture future avranno il corretto count (un count Ciò non si riserva lo spazio per le variabili non lette dai canali Gced).

Soluzione 1 - Gestione manuale delle risorse (cosa voglio evitare)

Il limite di JNB ha effettivamente gestito la gestione delle risorse manuali per questo motivo. Vedere il cancelBTChan. Vado a fare qualcosa di più difficile per l'utente per sbagliare (non che la gestione manuale non è il modo giusto di andare in molti casi).

Soluzione 2 - Usa le eccezioni bloccando su TVARS (GHC non può farlo come voglio)

Modifica questa soluzione e la soluzione 3 che è solo uno spin-off, non funziona! A causa di Bug 5055 (WontFix) Il compilatore GHC invia eccezioni a entrambi i thread bloccati, anche se uno è sufficiente (che è teoricamente determinabile, ma non pratico con GHC GC).

Se tutti i modi per ottenere un BTChan sono io, possiamo forkIO un thread che legge/si rivolge su un campo TVAR extra (fittizio) unico per il dato BTChan. Il nuovo thread prenderà un'eccezione quando tutti gli altri riferimenti al TVAR vengono eliminati, quindi saprà quando decremento il nrDups e singoli contatori di elementi. Questo dovrebbe funzionare ma costringe tutti i miei utenti a usare io per ottenere il loro BTChanS:

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ì, questo è un finalizzatore di poveri e non ho alcun motivo particolare per evitare di usare addFinalizer. Sarebbe la stessa soluzione, costringendo ancora l'uso di Io afaict.

Soluzione 3: un'API più pulita rispetto alla soluzione 2, ma GHC non la supporta ancora

Gli utenti iniziano un thread di manager chiamando initBTChanCollector, che monitorerà un set di questi TVAR fittizi (da Solution 2) e farà la pulizia necessaria. Fondamentalmente, spinge l'IO in un altro thread che sa cosa fare tramite un globale (unsafePerformIOed) TVar. Le cose funzionano praticamente come Solution 2, ma la creazione di Btchan può ancora essere STM. Mancata corsa initBTChanCollector comporterebbe un elenco in continua crescita (perdita di spazio) di compiti mentre il processo svolge.

Soluzione 4: non consentire mai di scartare BTChanS

Questo è simile a ignorare il problema. Se l'utente non lascia mai cadere un duplice BTChan Quindi il problema scompare.

Soluzione 5Vedo la risposta di Ezyang (totalmente valida e apprezzata), ma vorrei davvero mantenere l'attuale API solo con una funzione "DUP".

** Soluzione 6 ** Per favore dimmi che c'è un'opzione migliore.

EDIT: i Soluzione implementata 3 (rilascio di alfa totalmente non testato) e ha gestito la potenziale perdita di spazio rendendo il globale stesso a BTChan - che Chan dovrebbe probabilmente avere una capacità di 1, quindi dimenticarti di correre init Si presenta molto velocemente, ma questo è un cambiamento minore. Funziona in GHCI (7.0.3) ma questo sembra essere casuale. GHC lancia eccezioni a entrambi i thread bloccati (quello valido leggendo il btchan e il thread di visione) in modo che se sei bloccato leggendo un btchan quando un altro thread scarta il suo riferimento, allora muori.

È stato utile?

Soluzione

Ecco un'altra soluzione: richiedere tutti gli accessi al duplicato del canale limitato per essere racchiusa da una funzione che rilascia le sue risorse all'uscita (con un'eccezione o normalmente). Puoi usare una monade con un corridore di rango 2 per evitare che i canali duplicati perdono. È ancora manuale, ma il sistema di tipo rende molto più difficile fare cose cattive.

Non vuoi davvero fare affidamento su veri finalizzatori IO, perché GHC non fornisce garanzie su quando un finalizer può essere eseguito: per tutto quello che sai che potrebbe aspettare fino alla fine del programma prima di eseguire il finalizer, il che significa che sei bloccato fino ad allora.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top