[GHC] #9539: TQueue can lead to thread starvation
GHC
ghc-devs at haskell.org
Sun May 27 17:25:17 UTC 2018
#9539: TQueue can lead to thread starvation
-------------------------------------+-------------------------------------
Reporter: jwlato | Owner: (none)
Type: bug | Status: new
Priority: normal | Milestone:
Component: Core Libraries | Version: 7.8.2
Resolution: | Keywords: stm
Operating System: Unknown/Multiple | Architecture:
| Unknown/Multiple
Type of failure: None/Unknown | Test Case:
Blocked By: | Blocking:
Related Tickets: | Differential Rev(s):
Wiki Page: |
-------------------------------------+-------------------------------------
Comment (by YitzGale):
Here is an alternative safer proposal:
We retain the well-understood behavior and performance of the classic
functional queue. But we ensure that the amortized cost of the occasional
reverse is shared fairly by the read and write ends of the queue. We do
that by placing a singleton buffer in the middle, between the read and
write buffers. Whoever first finds the middle buffer unavailable pays the
price that time. Here is a simplistic initial implementation:
{{{#!hs
data TQueue a = TQueue !(TVar [a]) !(TMVar a) !(TVar [a])
readTQueue :: TQueue a -> STM a
readTQueue (TQueue read middle write) = readTVar read >>= \case
x:xs -> pure x <* writeTVar read xs
_ -> takeTMVar middle `orElse`
(readTVar write >>= \case
[] -> retry
ys -> do
writeTVar write []
let z:zs = reverse ys
pure z <* writeTVar read zs
)
writeTQueue :: TQueue a -> a -> STM ()
writeTQueue (TQueue read middle write) x =
(do
putTMVar middle x
readTVar write >>= \case
[] -> pure ()
ys -> do -- read must be empty in this case.
-- strict reverse to ensure we pay the amortization price
-- here and not in a read operation.
writeTVar read $! reverse ys
writeTVar write []
) `orElse` modifyTVar' write (x :)
}}}
The above code depends on the invariant that if the write buffer is non-
empty and the middle is empty, then the read buffer is empty. We can
observe that this is true because a read operation will only empty the
middle if there is no data left in the read buffer, and once that happens
the read buffer will remain empty until the next time that the write
buffer is emptied. But the type does not enforce that invariant.
Here is a safer implementation with a type which cannot represent the
impossible state, but requires a few more cheap TVar operations:
{{{#!hs
newtype TQueue a = TQueue (TVar (TQueue' a))
data TQueue' a =
TQueueR !(TVar [a]) -- middle and write are empty
| TQueueW !(TVar (NonEmpty a)) -- read and middle are empty
| TQueueM !(TVar [a]) !(TVar a) !(TVar [a]) -- middle is non-empty
readTQueue :: TQueue a -> STM a
readTQueue (TQueue tvq) = readTVar tvq >>= \case
TQueueR tvr -> readTVar tvr >>= \case
x:xs' -> pure x <* writeTVar tvr xs'
_ -> retry
TQueueW tvw -> do
ys <- readTVar tvw
let z :| zs = NE.reverse ys
pure z <* (newTVar zs >>= writeTVar tvq . TQueueR)
TQueueM tvr tvm tvw -> readTVar tvr >>= \case
x:xs' -> pure x <* writeTVar tvr xs'
_ -> readTVar tvm <*
(readTVar tvw >>= \case
y : ys -> newTVar (y :| ys) >>= writeTVar tvq . TQueueW
_ -> writeTVar tvq (TQueueR tvr) -- empty
)
writeTQueue :: TQueue a -> a -> STM ()
writeTQueue (TQueue tvq) x = readTVar tvq >>= \case
TQueueR tvr -> do
tvm <- newTVar x
tvw <- newTVar []
writeTVar tvq (TQueueM tvr tvm tvw)
TQueueW tvw -> do
ys <- readTVar tvw
-- strict reverse to ensure we pay the amortization price
-- here and not in a read operation.
tvr <- newTVar $! (reverse $ NE.toList ys)
tvm <- newTVar x
tvw' <- newTVar []
writeTVar tvq (TQueueM tvr tvm tvw')
TQueueM _ _ tvw -> modifyTVar' tvw (x :)
}}}
--
Ticket URL: <http://ghc.haskell.org/trac/ghc/ticket/9539#comment:18>
GHC <http://www.haskell.org/ghc/>
The Glasgow Haskell Compiler
More information about the ghc-tickets
mailing list