Brief summarize of shared state options, with a focus on the performance implications of the different choices.
Typically, the best approach when you want some shared state is to take an existing pure data structure, such as a list or a Map, and store it in a mutable container.
There are a couple of subtle performance issues to be aware of, though.
The first is the effect of lazy evaluation when writing a new value int othe container, which we've already covered.
The second is the choice of mutable contaienr itself, which exposes some subtle performance trade-offs. There are three choices:
-
MVar: we found in Limiting the Number of Threads with a Semaphore that using an MVar to keep a shared counter did not perform well under high contention. This is a consequence of the fairness guarantee that MVar offers.
-
TVar: sometimes performs better than MVar under contention and has the advantage of being composable with other STM operations. However, be aware of the other performance pitfalls with STM described in Performance.
-
IORef: using an IORef together with
atomicModifyIORef
is often a good choice for performance. The main pitfall here is lazy evaluation; getting enough strictness when usingatomicModifyIORef
is quite tricky. This is a good pattern to follow:
b <- atomicModifyIORef ref
(\x -> let (a, b) = f x
in (a, a `seq` b))
b `seq` return b
The seq
call on the last line forces the second component of the pair, which itself is a seq
call that forces a
, which in turn forces the call to f
.