I’m trying to understand the MVar example in the GHC latest docs –
data SkipChan a = SkipChan (MVar (a, [MVar ()])) (MVar ())
newSkipChan :: IO (SkipChan a)
newSkipChan = do
sem <- newEmptyMVar
main <- newMVar (undefined, [sem])
return (SkipChan main sem)
putSkipChan :: SkipChan a -> a -> IO ()
putSkipChan (SkipChan main _) v = do
(_, sems) <- takeMVar main
putMVar main (v, [])
mapM_ (sem -> putMVar sem ()) sems
getSkipChan :: SkipChan a -> IO a
getSkipChan (SkipChan main sem) = do
takeMVar sem
(v, sems) <- takeMVar main
putMVar main (v, sem:sems)
return v
dupSkipChan :: SkipChan a -> IO (SkipChan a)
dupSkipChan (SkipChan main _) = do
sem <- newEmptyMVar
(v, sems) <- takeMVar main
putMVar main (v, sem:sems)
return (SkipChan main sem)
I understand most of the program but for two questions –
- Are operations like
putSkipChanatomic? It seems to avoid blocking onputMVarby first doing atakeMVar. But wouldn’t that fail if something else callsputMVarafter thetakeMVarbut before theputMVar? In such cases, it seems the program would block forever. - Why does
dupSkipChanappendsemto the list of semaphores in theSkipChan? Isn’t that done bygetSkipChan. It seems to me that callingdupSkipChanfollowed bygetSkipChan(which seems to be what you have to do to have multiple readers) would cause a block whenputSkipChantries to wake up the same semaphore twice?
You are correct, another thread could call
putMVar mainand mess upputSkipChan. But the module creating the above code would not export theSkipChanconstructor so such a rogue operation would be impossible.dupSkipChanmakes a newemptyMVarcalledsemand adds that to the list in main. It does not add the pre-existing one that was created innewSkipChan. Thus there is no block.To explain more to other readers of this question and comment: The idea is that there may be multiple reader threads. Initially
SkipChan main sem1is the only such reader.dupSkipChanmakes aSkipChan main sem2. If there are thousands of readers then you would not want to notify all of them of a new value inputSkipChan, thus the design is thatgetSkipChanputs its sem into the list in main. InitializingSkipChanas done innewSkipChananddupSkipChanalso includes putting the new emptyseminto the list in main.The above initialization and design means that the first
getSkipChanobtains the most recent past value to have been written (or block for the first value to arrive). FuturegetSkipChanon thatSkipChanwill always get a newer value than any gotten before, and these will not block if that value is already available.