Interruptible exception wormholes kill modularity

Edward Z. Yang ezyang at mit.edu
Sat Jul 2 16:25:07 UTC 2016


Excerpts from Simon Marlow's message of 2016-07-02 05:58:14 -0400:
> > Claim 1: Here is some code which reimplements 'unblock':
> >
> >     import Control.Exception
> >     import Control.Concurrent
> >     import Control.Concurrent.MVar
> >
> >     unblock :: IO a -> IO a
> >     unblock io = do
> >         m <- newEmptyMVar
> >         _ <- forkIO (io >>= putMVar m)
> >         takeMVar m
> >
> >
> This isn't really an implementation of unblock, because it doesn't enable
> fully-asynchronous exceptions inside io.  If a stack overflow occurs, it
> won't be thrown, for example.  Also, io will not be interrupted by an
> asynchronous exception thrown to the current thread.

Oh, that's true. I suppose you could work around this by passing
on an asynchronous exception to a child thread that is unmasked
using forkIOWithUnmask, although maybe you would consider that
cheating?

> We already have a way to allow asynchronous exceptions to be thrown within
> a mask, it's called allowInterrupt:
> http://hackage.haskell.org/package/base-4.9.0.0/docs/Control-Exception.html#v:allowInterrupt

Well, it's different, right?  allowInterrupt allows asynchronous exceptions to
be thrown at a specific point of execution; unblock allows asynchronous
exceptions to be thrown at any point while the inner IO action is
executing.  I don't see why you would allow the former without the
latter.

> I don't buy the claim that this breaks "modularity".  The way to think
> about mask is that it disables fully-asynchronous exceptions, only allowing
> them to be thrown at certain well-defined points.  This makes them
> tractable, it means you can write code without worrying that an async
> exception will pop up at any point.  Inside a mask, the only way to get
> back to the state of fully asynchronous exceptions is to use the unblock
> action that mask gives you (provided you weren't already inside a mask).

I suppose what I don't understand, then, is that if interruptible
points are modular, I don't see why unblock isn't modular either;
it's just a more convenient way of inserting allowInterrupt between
every indivisible IO operation in user code.

> > You could very well argue that interruptible actions are a design flaw.
> >
> 
> I disagree - it's impossible to define withMVar without interruptible mask.

What about this version of withMVar using uninterruptible? (Assume
no other producers.)

    withMVarUninterruptible :: MVar a -> (a -> IO b) -> IO b
    withMVarUninterruptible m io =
      uninterruptibleMask $ \restore -> do
        a <- restore (takeMVar m)
        b <- restore (io a) `onException` putMVar m a
        putMVar m a
        return b

I don't think it is quite right, as there is race between when
takeMVar unblocks, and when the uninterruptible mask is restored.
But perhaps the primary utility of interruptible masks is to
let you eliminate this race.

> > Then you should use 'uninterruptibleMask' instead, which effectively
> > removes the concept of interruptibility--and is thus modular.  Indeed,
> > Eyal Lotem proposed [2] that 'bracket' should instead use
> > 'uninterruptibleMask', for precisely the reason that it is too easy to
> > reenable asynchronous exceptions in 'mask'.
> 
> 
> The problem he was talking about was to do with the interruptibility of the
> cleanup action in bracket, not the acquire, which really needs
> interruptible mask. The interruptibility of the cleanup is a complex issue
> with arguments on both sides.  Michael Snoyman recently brought it up again
> in the context of his safe-exceptions library.  We might yet change that -
> perhaps at the very least we should implement a catchUninterruptible# that
> behaves like catch# but applies uninterruptibleMask to the handler, and
> appropriate user-level wrappers.

Yes, it is complex, and I won't claim to know the right answer here.

> > But assuming that
> > interruptible masks are a good idea (Simon Marlow has defended them
> > as "a way avoid reasoning about asynchronous exceptions except
> > at specific points, i.e., where you might block"), there should
> > be an 'unblock' for this type of mask.
> >
> > It should be said that the absence of 'unblock' for
> > 'uninterruptibleMask' only implies that a passed in IO action (e.g., the
> > cleanup action in bracket) does not have access to the exceptions thrown
> > to the current thread; it doesn't actually guarantee uninterruptibility,
> > since the passed in IO action could always raise a normal exception.
> > Haskell's type system is not up to the task of enforcing such
> > invariants.
> >
> > Cheers,
> > Edward
> >
> > [1]
> > https://mail.haskell.org/pipermail/libraries/2010-March/013310.html
> > https://mail.haskell.org/pipermail/libraries/2010-April/013420.html
> >
> > [2]
> > https://mail.haskell.org/pipermail/libraries/2014-September/0
> > <https://mail.haskell.org/pipermail/libraries/2014-September/023675.html>
> > Cheers,
> > Simon
> >
> > 23675.html
> > <https://mail.haskell.org/pipermail/libraries/2014-September/023675.html>
> >
> > P.S. You were CC'ed to this mail because you participated in the original
> > "Asynchronous exception wormholes kill modularity" discussion.
> >
> > P.P.S. I have some speculations about using uninterruptibleMask more
> > frequently: it seems to me that there ought to be a variant of
> > uninterruptibleMask that immediately raises an exception if
> > the "uninterruptible" action blocks.  This would probably of
> > great assistance of noticing and eliminating blocking in
> > uninterruptible code.
> >
> 
> Now that's an interesting idea!

It is too bad that it is far too difficult to let Haskell-land
iterate and try these things out: need RTS cooperation.

Edward


More information about the ghc-devs mailing list