packages feed

cleff-0.3.3.0: src/Cleff/Internal/ThreadVar.hs

{-# LANGUAGE CPP           #-}
{-# LANGUAGE UnboxedTuples #-}
{-# OPTIONS_HADDOCK not-home #-}
-- |
-- Copyright: (c) 2021 Xy Ren
-- License: BSD3
-- Maintainer: xy.r@outlook.com
-- Stability: unstable
-- Portability: non-portable (GHC only)
--
-- This module contains a contention-free thread-local variable datatype.
--
-- __This is an /internal/ module and its API may change even between minor versions.__ Therefore you should be
-- extra careful if you're to depend on this module.
module Cleff.Internal.ThreadVar (ThreadVar, newThreadVar, getThreadVar) where

import           Cleff.Internal
import           Control.Monad.IO.Class (MonadIO (liftIO))
import           Data.Atomics           (atomicModifyIORefCAS_)
import           Data.IntMap.Strict     (IntMap)
import qualified Data.IntMap.Strict     as Map
import           Foreign.C.Types
import           GHC.Conc               (ThreadId (ThreadId))
import           GHC.Exts               (ThreadId#, mkWeak#)
import           GHC.IO                 (IO (IO))
import           UnliftIO.Concurrent    (myThreadId)
import           UnliftIO.IORef         (IORef, newIORef, readIORef)

-- | Get the hash for a 'ThreadId' in terms of C types (RTS function).
#if __GLASGOW_HASKELL__ >= 903
foreign import ccall unsafe "rts_getThreadId"
  getThreadId :: ThreadId# -> CULLong
#elif __GLASGOW_HASKELL__ >= 900
foreign import ccall unsafe "rts_getThreadId"
  getThreadId :: ThreadId# -> CLong
#else
foreign import ccall unsafe "rts_getThreadId"
  getThreadId :: ThreadId# -> CInt
#endif

-- | Generates a numeric hash for a 'ThreadId'. Before GHC 9.4, this function has a practical possibility of hash
-- collision on 32-bit or Windows platforms, if threads are created rapidly and thread count exceeds 2^32. After GHC
-- 9.4, this function practically won't produce collision as the hash is extended to 64-bit on all platforms.
hashThreadId :: ThreadId -> Int
hashThreadId (ThreadId tid#) = fromIntegral (getThreadId tid#)

-- | Attach a finalizer (an 'IO' computation) to a thread.
attachFinalizer :: ThreadId -> IO () -> IO ()
attachFinalizer (ThreadId tid#) (IO finalize#) = IO \s1 -> let
  !(# s2, _ #) = mkWeak# tid# () finalize# s1
  in (# s2, () #)

-- | A thread-local variable. It is designed so that any operation originating from existing threads produce no
-- contention; thread contention only occurs when multiple new threads attempt to first-time access the variable
-- at the same time.
data ThreadVar a = ThreadVar a {-# UNPACK #-} !(IORef (IntMap (IORef a)))

-- | Create a thread variable with a same initial value for each thread.
newThreadVar :: MonadIO m => a -> m (ThreadVar a)
newThreadVar x = ThreadVar x <$> newIORef Map.empty

-- | Get the variable local to this thread, in the form of an 'IORef'. It is guaranteed that the returned 'IORef'
-- will not be read or mutated by other threads inadvertently.
getThreadVar :: MonadIO m => ThreadVar a -> m (IORef a)
getThreadVar (ThreadVar x0 table) = do
  tid <- myThreadId
  let thash = hashThreadId tid
  maybeRef <- Map.lookup thash <$> readIORef table
  case maybeRef of
    Nothing -> do
      ref <- newIORef x0
      liftIO $ noinline atomicModifyIORefCAS_ table (Map.insert thash ref)
      liftIO $ attachFinalizer tid $
        noinline atomicModifyIORefCAS_ table (Map.delete thash)
      pure ref
    Just ref -> pure ref
{-# INLINE getThreadVar #-}