[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