MVar
is, like you said, targeted at multithreading while IORef
can be used both as mutable variable in a single threaded program or as a synchronization construct in a multithreaded program.
IORef
can be used together with atomicModifyIORef
to get compare-and-swap (CAS) behavior: writers and readers can synchronize on a single pure value, stored in the IORef
. Readers use readIORef
to read a value and writers use atomicModifyIORef
to write a value. Note that atomicModifyIORef
doesn't let writers perform any side effects inside the critical section (i.e. they can only use a pure function when atomically changing the value).
MVar
allows you to implement arbitrary critical sections (using withMVar
), that may contain side effects. They can also be used just like an IORef
(as described in the previous paragraph), but at a higher cost.
If you want an intuition for what kind of semantic IORef
implements its the same as the CAS semantics Rich Hickey describes in a talk on Clojure's concurrency model: http://www.infoq.com/presentations/Are-We-There-Yet-Rich-Hickey
Edit: In addition, you cannot run into deadlocks using IORef
(but there still might be contention, causing retries).