{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {- | Module: Control.Concurrent.FVar Description: Fast (partly lockless) mutable Vars Copyright: (c) Erick Gonzalez, 2019 License: BSD3 Maintainer: erick@codemonkeylabs.de This library implements an "FVar" or /fast Var/. The idea is that it allows concurrent readers to access shared data without locking, but the moment a writer performs a write it gets exclusive access to the var. If there are readers currently accesing the data, it waits until they complete the operation, blocks any further reads and then it performs the write. Once it is done, it reenables the stalled readers and lockless operation continues. -} module Control.Concurrent.FVar (modifyFVar, newFVar, readFVar, FVar) where import Control.Concurrent.MVar import Control.Monad (replicateM_, when) import Control.Monad.Catch (bracket, bracket_, MonadMask) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.IORef import Data.Maybe (isJust) import System.IO.Unsafe (unsafePerformIO) -- | An 'FVar' (a.k.a. /eff-var/ or /Fast Var/) is a synchronising variable which allows -- concurrent simultaneous read access without the need for locking. Write access is however -- exclusive and causes all other operations to stall until the write is completed and -- lockless operation can continue. newtype FVar a = FVar { iRef :: IORef (RWVar a) } deriving (Eq) data RWVar a = RWVar { readLock :: Maybe (MVar ()), readMutex :: MVar (), writeMutex :: MVar (), readersWaiting :: Int, writersWaiting :: Int, readers :: Int, writers :: Int, unboxed :: a } #ifdef DEBUG {-# NOINLINE out #-} out :: MVar () out = unsafePerformIO $ newMVar () debug :: (MonadIO m) => String -> m () debug = liftIO . withMVar out . const . putStrLn #else debug :: (MonadIO m) => String -> m () debug = const $ return () #endif -- | Creates a new 'FVar' containing the data provided. Note that unlike 'MVar's, -- an 'FVar' can not be empty. This is to avoid complications stalling lock-less reads -- in that case. There is of course nothing stopping you from storing a value wrapped in -- a 'Maybe' to account for a similar usage pattern. newFVar :: (MonadIO m) => a -> m (FVar a) newFVar val = liftIO $ do rMVar <- newEmptyMVar wMVar <- newEmptyMVar ref <- newIORef RWVar { readLock = Nothing, readersWaiting = 0, writersWaiting = 0, readers = 0, writers = 0, readMutex = rMVar, writeMutex = wMVar, unboxed = val } return $ FVar ref -- | Access an 'FVar' for reading using the provided function. Access is __not__ exclusive, -- so multiple threads would be allowed to access the data simultaneously. Note that there is -- actually nothing in principle preventing one from mutating shared data during this access -- but in doing so you would be violating the contract in this API and thus integrity could -- not be guaranteed, so you must refrain from doing so and truly only perform read accesses -- to the shared data readFVar :: (MonadMask m, MonadIO m) => FVar a -> (a -> m b) -> m b readFVar FVar {..} = bracket wantsRead readDone where wantsRead = do (x, mLock) <- liftIO $ atomicModifyIORef' iRef readerIn maybe (access x) acquire mLock access x = do debug "reader reads fast" return x readDone _ = do debug "reader done" mLock <- liftIO $ atomicModifyIORef' iRef $ readerOut mapM_ release mLock acquire var = do debug "reader waits" liftIO $ do takeMVar var debug "reader acquired lock" unboxed <$> readIORef iRef release var = do debug "reader released write lock" liftIO $ putMVar var () -- | Modify the data inside an 'FVar'. Write access during execution of the function provided -- will be exclusive so other writes or reads will be stalled until the call to 'modifyFVar' -- is completed modifyFVar :: (MonadMask m, MonadIO m) => FVar a -> (a -> m (a, b)) -> m b modifyFVar FVar {..} mutator = bracket_ wantsWrite writeDone mutate where wantsWrite = liftIO $ mapM_ acquire =<< atomicModifyIORef' iRef writerIn writeDone = liftIO $ do debug "writer done" mLock <- atomicModifyIORef' iRef writerOut mapM_ release mLock acquire var = do debug "writer waits" takeMVar var debug "writer acquired lock" release (waiters, var) = do debug "writer released lock" liftIO $ replicateM_ waiters $ putMVar var () mutate = do debug "writer writes" (value, result) <- mutator =<< unboxed <$> liftIO (readIORef iRef) liftIO . atomicModifyIORef' iRef $ \var -> (var { unboxed = value }, result) readerIn :: RWVar a -> (RWVar a, (a, Maybe (MVar ()))) readerIn var@RWVar {..} = (var { readers = readers + 1, readersWaiting = waitingNow }, (unboxed, readLock)) where waitingNow | locked = readersWaiting + 1 | otherwise = readersWaiting locked = isJust readLock readerOut :: RWVar a -> (RWVar a, Maybe (MVar ())) readerOut var@RWVar {..} = (var { readers = readersNow, writersWaiting = waitingNow}, withLock) where withLock | unlockWriter = return writeMutex | otherwise = Nothing readersNow = readers - 1 waitingNow | unlockWriter = writersWaiting - 1 | otherwise = writersWaiting unlockWriter = readersNow == readersWaiting && writersWaiting /= 0 writerIn :: RWVar a -> (RWVar a, Maybe (MVar ())) writerIn var@RWVar {..} = (var { readLock = return readMutex, writers = writers + 1, writersWaiting = waitingNow }, withLock) where waitingNow | stalled = writersWaiting + 1 | otherwise = writersWaiting withLock | stalled = return writeMutex | otherwise = Nothing stalled = writers /= 0 || readers /= readersWaiting writerOut :: RWVar a -> (RWVar a, Maybe (Int, MVar ())) writerOut var@RWVar {..} = (var { readLock = rearmLock, readersWaiting = 0, writers = writers - 1, writersWaiting = nowWaiting }, withLock) where rearmLock | stalledWriters = return readMutex | otherwise = Nothing withLock | stalledReaders = return (readersWaiting, readMutex) | stalledWriters = return (1, writeMutex) | otherwise = Nothing nowWaiting | stalledWriters && not stalledReaders = writersWaiting - 1 | otherwise = writersWaiting stalledReaders = readersWaiting /= 0 stalledWriters = writersWaiting /= 0