The Revenge of Finalizers

George Russell ger at tzi.de
Thu Oct 17 09:21:30 EDT 2002


Alastair Reid wrote:
> 
> Alastair:
> > > I don't know how to achieve the same goal with atomicModifyIORef.
> 
> George:
> > I do.  To modify ioRef1 and ioRef2 "simultaneously", write
> >
> >   atomicModifyIORef ioRef1  (\ contents1 ->
> >      unsafePerformIO ioRef2 (\ contents2 ->
> >      blah blah))
> >
> > The actual modification will take place when the result or contents of
> > ioRef1 or ioRef2 get evaluated.
> 
> I must be missing something because this seems to be riddled with race
> conditions.
> 
> In particular, if ioRef1 is updated by a lazy function, then the write
> to ioRef1 happens but the write to ioRef2 does not.
Yes but so what?  The write to ioRef2 will happen when you try to evaluate
something from the unsafePerformIO.  You can do that right away (seq'ing whatever
the first atomicModifyIORef returned) if it makes you feel happier.

EG (to take an actual example of something I use to implement composable events in
Haskell) we can implement a function

simpleToggle2 :: IORef Bool -> IORef Bool -> IO (Just (Bool,Bool))

which attempts to flip the two IORefs from True to False (if they are both True);
otherwise returning their actual values.  Then you could code this something like this
(no I'm not going to check if it passes GHC)

simpleToggle2 ioRef1 ioRef2 =
   do
      res <- atomicModifyIORef ioRef1 (\ contents1 ->
         unsafePerformIO (atomicModifyIORef ioRef2 (\ contents2 ->
            if contents1 && contents2 
               then
                 (False,(False,Nothing))
               else
                 (contents2,(contents1,Just (contents1,contents2)))
            )
         )
      seq res (return res)

Then the first atomicModifyIORef replaces the contents of ioRef1 by a thunk, and
returns another thunk.  The seq then proceeds to evaluate this thunk, causing
the unsafePerformIO to be run, which changes ioRef2.

There IS a potential problem, because if you run simpleToggle2 on the same ioRef

simpleToggle2 ioRef ioRef

or if two threads run simpleToggle2 simultaneously on the same ioRefs but in opposite order

simpleToggle2 ioRef1 ioRef2 || simpleToggle2 ioRef2 ioRef1

or in general you have n threads simultaneously running simpleToggle on a cycle of ioRefs,
you can get non-termination, because you will find your thunks get linked in a circular chain,
with each one depending on the next.  This is exactly the same as what happens if you attempt

writeIORef x (unsafePerformIO (readIORef x))

and then attempt to evaluate the contents of x.  However you also have exactly the same problem
when you try to implement simpleToggle2 on MVar Bool, except there you get deadlock instead.
The solution in both cases is to put some constraint on the ordering, for example construct some
well-founded linear order on ioRefs such that simpleToggle2 ioRef1 ioRef2 is never called unless
ioRef2 < ioRef1 (I think it's that way round).

If you all you have is a data structure containing two IORefs, and the only accesses you want
to these IORefs are (a) individual access, with a guaranteed terminating update function;
(b) joint access, then the solution is simply to make sure that (say) you always access the first
ioRef first.  Then I think you have guaranteed termination.  This is exactly the same with MVars.
So although some Haskell purists (mentioning no names) might object to using unsafePerformIO
like this, it does not seem to make matters worse than using MVars, since it's purely a question
of taste whether you prefer non-termination or a deadlock.



More information about the FFI mailing list