| Safe Haskell | None |
|---|---|
| Language | Haskell2010 |
Control.Concurrent.Thread.Storage
Description
Thread-local storage for Haskell green threads.
Associates at most one value of type a with each green thread in a
ThreadStorageMap. Values are automatically cleaned up by a GC finalizer
when the owning thread dies.
Implementation
Internally, a ThreadStorageMap is a flat open-addressed hash table that
resizes automatically when full. Keys (thread IDs) live in a
MutableByteArray# with per-slot atomic CAS; values live in a GC-traced
MutableArray# of IORefs. On resize, a new table is allocated at
double the capacity, live entries are copied (cleaning tombstones), and
the reference is swapped under an MVar lock that serializes resize
operations; at most one thread performs the expensive copy-and-swap at a
time while other inserters wait. In-flight readers on the old table are
safe because the old arrays remain valid GC objects and the per-thread
IORefs are shared between old and new tables.
Reads and writes on the hot path go directly to the per-thread IORef,
with zero CAS and zero contention. CAS is only used during thread registration
(once per thread lifetime) and during finalizer-driven cleanup.
Two CMM primops avoid allocation and FFI overhead on the hot path:
stg_getCurrentThreadId: readsStgTSO_id(CurrentTSO)directly.stg_probeThreadSlot: fuses thread-ID retrieval with a multiplicative-hash linear probe of the key array.
Slot hashing
Slot assignment uses a Fibonacci/golden-ratio multiplicative hash
(tid * 0x9E3779B97F4A7C15) rather than a simple bit-mask. This spreads
sequential thread IDs (GHC allocates them contiguously) across different
cache lines, eliminating false sharing on both the key and value arrays
under multi-core contention.
Detach encoding
Thread IDs are 32-bit (StgWord32) but stored in 64-bit key slots.
Bit 32 serves as a "detached" flag. When a context is detached via
detach, the flag is set in the key array (a single atomic write to
unboxed memory — no GC write barrier, no card-table contention). The
value slot is left untouched so no MutableArray# card is dirtied.
The CMM probe reports detach status via its return value, so the
Haskell hot path for lookup and adjust never checks the value
array for detached markers at all.
Choosing an API tier
This module exposes three tiers of API, from simplest to fastest:
- High-level
attach,detach,lookup,update,adjustand their…OnThreadvariants. Each call resolves the thread ID internally. Fine when you make only one or two calls per operation.- Raw
getThreadId/lookupRaw/updateRaw. Pre-compute the thread-ID word once, then pass it to several operations on the same thread without repeated FFI calls.- Ref-based
ensureRefFast/lookupRefFast/readRef/writeRef/modifyRef. On the fast path (thread already registered), the entire lookup is a single CMM call plus anIORefdereference. Subsequent reads and writes are plainIORefoperations with no hash-table probe at all. Use this tier in instrumentation hot loops (e.g. tracing spans).
Lifecycle
- A value
attached to a thread remains reachable at least as long as the thread is alive. - A value may be explicitly removed via
detachat any time. The hash-table key is marked with a "detached" bit; the value slot is not overwritten. A subsequentattachon the same thread reuses the slot without registering a duplicate GC finalizer. - After a thread dies, its finalizer tombstones the slot. The
IORef(and the value it holds) become eligible for GC once no other references remain. purgeDeadThreadscan be used to eagerly reclaim slots for threads that have exited but whose finalizers have not yet run. (GHC >= 9.6 only.)
Synopsis
- data ThreadStorageMap a
- newThreadStorageMap :: MonadIO m => m (ThreadStorageMap a)
- newThreadStorageMapWith :: MonadIO m => Int -> m (ThreadStorageMap a)
- lookup :: MonadIO m => ThreadStorageMap a -> m (Maybe a)
- lookupOnThread :: MonadIO m => ThreadStorageMap a -> ThreadId -> m (Maybe a)
- attach :: MonadIO m => ThreadStorageMap a -> a -> m (Maybe a)
- attachOnThread :: MonadIO m => ThreadStorageMap a -> ThreadId -> a -> m (Maybe a)
- detach :: MonadIO m => ThreadStorageMap a -> m (Maybe a)
- detachFromThread :: MonadIO m => ThreadStorageMap a -> ThreadId -> m (Maybe a)
- update :: MonadIO m => ThreadStorageMap a -> (Maybe a -> (Maybe a, b)) -> m b
- updateOnThread :: MonadIO m => ThreadStorageMap a -> ThreadId -> (Maybe a -> (Maybe a, b)) -> m b
- adjust :: MonadIO m => ThreadStorageMap a -> (a -> a) -> m ()
- adjustOnThread :: MonadIO m => ThreadStorageMap a -> ThreadId -> (a -> a) -> m ()
- getThreadId :: ThreadId -> Word
- getCurrentThreadId :: IO Int
- lookupRaw :: MonadIO m => ThreadStorageMap a -> Word -> m (Maybe a)
- updateRaw :: MonadIO m => ThreadStorageMap a -> ThreadId -> Word -> (Maybe a -> (Maybe a, b)) -> m b
- ensureRef :: ThreadStorageMap a -> ThreadId -> Int -> a -> IO (IORef a)
- ensureRefFast :: ThreadStorageMap a -> a -> IO (Int, IORef a)
- lookupRef :: ThreadStorageMap a -> Int -> IO (Maybe (IORef a))
- lookupRefFast :: ThreadStorageMap a -> IO (Int, Maybe (IORef a))
- readRef :: IORef a -> IO a
- writeRef :: IORef a -> a -> IO ()
- modifyRef :: IORef a -> (a -> a) -> IO ()
- storedItems :: ThreadStorageMap a -> IO [(Int, a)]
- purgeDeadThreads :: MonadIO m => ThreadStorageMap a -> m ()
The map type
data ThreadStorageMap a Source #
A concurrent map from green-thread IDs to values of type a.
Each thread may have at most one associated value. The table starts at
an initial capacity (see newThreadStorageMap, newThreadStorageMapWith)
and doubles automatically when full. Resize operations are serialized by
an internal MVar lock so that at most one thread performs the expensive
copy-and-swap at a time; other threads that discover a full table block
on the lock and retry after the resize completes.
All read paths and ref-based hot-path operations are entirely lock-free.
The MVar is only contended during table growth, which happens
O(log n) times over the life of the map.
Construction
newThreadStorageMap :: MonadIO m => m (ThreadStorageMap a) Source #
Create a ThreadStorageMap with a default initial capacity derived from
the number of runtime capabilities: max 128 (capabilities * 32), rounded
up to the next power of two.
The table resizes automatically when full, so this is a good default for most applications.
newThreadStorageMapWith :: MonadIO m => Int -> m (ThreadStorageMap a) Source #
Create a ThreadStorageMap with at least the given number of initial
slots.
The actual capacity is rounded up to the next power of two (minimum 16). The table doubles automatically when it runs out of slots. A load factor below 0.7 keeps probe chains short; resizing also cleans tombstones.
High-level API
Convenient functions that resolve the current thread's identity internally.
Each call obtains the ThreadId (or numeric ID) on your behalf, which is
fine for one-shot operations. If you are making multiple calls in sequence
for the same thread, consider the Raw API or Ref-based API
to avoid redundant work.
Lookup
lookup :: MonadIO m => ThreadStorageMap a -> m (Maybe a) Source #
Retrieve the value associated with the current thread, if any.
Uses the fused CMM probe which reads CurrentTSO.id, applies the
multiplicative hash, and linearly probes the key array in a single
CMM call. Returns Nothing for both absent and detached entries
without touching the value array in the detached case.
lookupOnThread :: MonadIO m => ThreadStorageMap a -> ThreadId -> m (Maybe a) Source #
Retrieve the value associated with a specific thread.
Insert / replace
attach :: MonadIO m => ThreadStorageMap a -> a -> m (Maybe a) Source #
Associate a value with the current thread, replacing any previous value.
Returns the previous value, or Nothing if the thread had no entry.
A GC finalizer is registered on the first call per thread so that the
entry is automatically cleaned up when the thread dies.
On the hot path (value already attached), no ThreadId is allocated and
no FFI call is made. myThreadId is only called on the cold first-insert
path to register the GC finalizer.
attachOnThread :: MonadIO m => ThreadStorageMap a -> ThreadId -> a -> m (Maybe a) Source #
Like attach, but targets a specific thread.
Remove
detach :: MonadIO m => ThreadStorageMap a -> m (Maybe a) Source #
Remove the value associated with the current thread.
Returns the removed value, or Nothing if the thread had no entry.
The slot key is marked with the detached bit (a single atomic write to
unboxed memory with no GC write barrier) so it can be reused by a
future attach without registering a duplicate GC finalizer.
detachFromThread :: MonadIO m => ThreadStorageMap a -> ThreadId -> m (Maybe a) Source #
Like detach, but targets a specific thread.
General update
update :: MonadIO m => ThreadStorageMap a -> (Maybe a -> (Maybe a, b)) -> m b Source #
Atomically read and update the value for the current thread.
The callback receives the current value (or Nothing) and returns a pair
of the new value to store (or Nothing to remove the entry) and an
arbitrary result.
Uses the fused CMM probe (stg_probeThreadSlot#). The probe reports
attached/detached/absent via its return encoding, so the hot path
(attached, updating the value) never checks the detached state at all.
-- Increment a counter, inserting 1 if absent: update tsm (\old -> (Just (maybe 1 (+1) old), ()))
updateOnThread :: MonadIO m => ThreadStorageMap a -> ThreadId -> (Maybe a -> (Maybe a, b)) -> m b Source #
Like update, but targets a specific thread.
This is the most general function in the high-level API.
attachOnThread and detachFromThread are implemented in terms of this.
In-place modification
adjust :: MonadIO m => ThreadStorageMap a -> (a -> a) -> m () Source #
Modify the value for the current thread in place if one is attached.
Does nothing if the thread has no entry or the entry is detached.
The modification is strict (modifyIORef'). Uses the fused CMM probe.
adjustOnThread :: MonadIO m => ThreadStorageMap a -> ThreadId -> (a -> a) -> m () Source #
Like adjust, but targets a specific thread.
Raw API
Pre-compute a thread's numeric ID once and reuse it across several
operations, avoiding repeated FFI calls to rts_getThreadId.
tid <- myThreadId let !tw =getThreadIdtidlookupRawtsm tw >>= \case ...updateRawtsm tid tw (\old -> ...)
The ThreadId is still required by updateRaw because it may need to
register a GC finalizer on the first insert.
getThreadId :: ThreadId -> Word Source #
Extract the numeric thread ID from an existing ThreadId.
This makes a cheap FFI call to rts_getThreadId. When you already hold a
ThreadId and need its numeric form for lookupRaw or updateRaw, use
this. Otherwise prefer getCurrentThreadId.
getCurrentThreadId :: IO Int Source #
Read the current green thread's numeric ID directly from CurrentTSO.
This is implemented as a CMM primop, so no ThreadId box is allocated and
no FFI call is made. Prefer this over
whenever you do not need the getThreadId =<< myThreadIdThreadId value itself.
lookupRaw :: MonadIO m => ThreadStorageMap a -> Word -> m (Maybe a) Source #
Retrieve a value using a pre-computed thread ID (from getThreadId).
Avoids the FFI call to rts_getThreadId that lookupOnThread would
make internally. Uses a CMM primop for the key-array probe.
updateRaw :: MonadIO m => ThreadStorageMap a -> ThreadId -> Word -> (Maybe a -> (Maybe a, b)) -> m b Source #
Generalized update using a pre-computed thread ID.
Behaves like updateOnThread but skips the internal getThreadId call.
The ThreadId argument is still needed so a GC finalizer can be
registered when a new entry is created. Uses a CMM primop for the
key-array probe.
Ref-based API
The fastest tier. On the hot path (thread already registered), the
operations below avoid the hash-table probe entirely by handing you the
per-thread IORef directly. Subsequent reads and writes are plain
IORef operations.
Typical usage in a tracing library:
-- Once per request (or per thread lifetime): (tid, ref) <-ensureRefFasttsm Nothing -- On every span open (hot path, no probe, no CAS):writeRefref (Just spanContext) -- On every span close: ctx <-readRefrefwriteRefref Nothing
If you already have a ThreadId and numeric ID, use ensureRef or
lookupRef. If you want the absolute fastest path and don't have a
ThreadId yet, use ensureRefFast or lookupRefFast which read
CurrentTSO.id and probe the key array entirely in CMM.
ensureRef :: ThreadStorageMap a -> ThreadId -> Int -> a -> IO (IORef a) Source #
Get or create the IORef for a given thread.
If the thread already has an entry, returns its IORef (read-only probe,
no CAS). Otherwise, creates a new IORef initialised to def, claims a
slot via CAS, and registers a GC finalizer for cleanup.
The Int argument is the numeric thread ID (e.g. from
getCurrentThreadId or fromIntegral . ).getThreadId
ensureRefFast :: ThreadStorageMap a -> a -> IO (Int, IORef a) Source #
Fused CMM fast path: get or create the IORef for the current thread.
Returns (threadId, ref).
Steady state (entry exists): read the table IORef, then a single
CMM call reads CurrentTSO.id and linearly probes the key array, then
one readArray# fetches the IORef. No ThreadId allocation, no FFI,
no Maybe wrapper.
First call per thread: falls back to myThreadId, CAS-inserts a new
IORef initialised to def, and registers a finalizer.
lookupRef :: ThreadStorageMap a -> Int -> IO (Maybe (IORef a)) Source #
Look up the IORef for a thread by its numeric ID (Haskell-side probe).
Use this when you already have the numeric ID but not necessarily the current thread's TSO (e.g. inspecting another thread's slot).
lookupRefFast :: ThreadStorageMap a -> IO (Int, Maybe (IORef a)) Source #
Look up the IORef for the current thread using the fused CMM probe.
Returns (threadId, . The numeric thread ID is
returned so you can pass it to Maybe (IORef a))ensureRef on the slow path without a
second FFI call:
(tid, mref) <-lookupRefFasttsm ref <- case mref of Just r -> pure r Nothing -> do t <- myThreadIdensureReftsm t tid defaultValue
modifyRef :: IORef a -> (a -> a) -> IO () Source #
Strictly modify the value in a per-thread IORef.
Equivalent to modifyIORef'.
Monitoring
storedItems :: ThreadStorageMap a -> IO [(Int, a)] Source #
Snapshot all live entries as (threadId, value) pairs.
Intended for monitoring and debugging, e.g. verifying that entries are cleaned up after threads exit. The result is a point-in-time snapshot; concurrent mutations may or may not be reflected.
purgeDeadThreads :: MonadIO m => ThreadStorageMap a -> m () Source #
Tombstone slots belonging to threads that are no longer alive, and shrink the table if the load factor drops below 25%.
Normally, slots are cleaned up by GC finalizers attached to the owning
ThreadId. This function provides an eager alternative: it calls
listThreads to obtain the set of live threads and tombstones
any slot whose key is not in that set.
Internally builds a flat array of live thread IDs and passes it to a
C function that qsorts it, then batch-scans the key array using
SIMD (NEON / SSE2) linear search for small live sets or branchless
binary search (Khuong / Lemire CMOV style) for large ones. A single
unsafe ccall amortises FFI overhead across the full table scan.
Tombstoning (key + value slot) is done on the Haskell side to
maintain GC write barriers.
After tombstoning, if the number of remaining live entries is less
than 1/4 of the table capacity (and the capacity exceeds the 16-slot
minimum), the table is rehashed to a smaller power-of-two size under
the resize MVar lock. This prevents unbounded memory use after
bursts of short-lived threads.
This is a best-effort operation: if a resize occurs concurrently, some dead entries may survive in the new table until the next purge or GC.
@since base 4.18.0 (GHC 9.6)