CAS fails with newly created ticket
Here I create a new IORef, get a ticket and immediately compare-and-swap. I expect that to succeed and thus the resulting boolean to be True, but it is actually False, regardless of optimization levels. The following features seem relevant, and removing any of them results in the output being True again:
- the newtype
T - binding the initial constant (to be put in the
IORef) as anINLINEvariable (surprisingly, either removing theINLINEpragma or inliningzeroby hand make the resultTrue) - forcing the ticket before the CAS.
What is wrong here?
import Data.IORef
import Data.Atomics
newtype T = T Int
zero :: T
zero = T 0
{-# INLINE zero #-}
main = do
x <- newIORef zero
ticket <- readForCAS x
(b, _) <- ticket `seq` casIORef x ticket (T 1)
print b
I noticed it returns False for both ghc 8.0 and 8.6, and latest atomic-primops. Didn't try others.
Also, maybe https://github.com/rrnewton/haskell-lockfree/issues/53 ?
Forcing a ticket really never makes sense the way tickets are defined. The strictness analyzer can get hold of this and say
T (I# tick) <- readForCAS x
(b, _) <- casIORef x (I# tick) (T 1)
All this unsafeCoerce# business is very flaky. It makes much more sense to use lazy (see #79). The only way I can see to avoid trouble when someone forces tickets is to make Ticket a datatype instead of a newtype, but then users had better be careful to be strict enough to make sure they unbox, or risk CAS failures because of allocation and so on. Sigh.
I'm really quite convinced that making Ticket a datatype is the correct solution. I've opened #86 to do that. Yes, if things don't unbox that creates a risk of CAS failing, but even that won't be nearly as catastrophic as how they can fail today.
Yes, if things don't unbox that creates a risk of CAS failing
Does that mean that something like atomicModifyIORefCAS Might Loop forever?
@jberryman That's always a risk in a lock-free context—there's no fairness guarantee. But in practice it shouldn't. Even if code takes a little longer than it should between read and CAS, it should eventually go through.
But I interpreted what you wrote to mean that if atomicModifyIORefCAS say is implemented in such a way that Ticket is not unboxed, then it would always loop forever as the CAS can never succeed, in other words would be a completely broken implementation. is that right?
No, that's not right. The only risk if the ticket is not unboxed is that there will be a longer delay between the read and the CAS, since a (two word) heap object will be allocated and the read value stored in and then retrieved from it. In a high-contention environment, that could cause excessive CAS misses, but it shouldn't cause permanent failure.