Fork a new thread to do some action with the socket
Ensure that the socket always gets closed once the thread ends
On the surface this seems really similar to what bracket does, but the forking throws a wrench into it.
Here’s my attempt, largely based on some code from network-simple, to abstract this into a general function:
bracketFork
:: IO resource
-- ^ Runs first, in the current thread
-> (resource -> IO a)
-- ^ Runs last, might run in either thread
-> (resource -> IO b)
-- ^ Runs in a new thread
-> IO ThreadId
bracketFork open close action = mask $ \restore ->
do
resource <- restore open
let a = action resource >> return ()
let b = close resource
forkIO (restore a `finally` b) `onException` b
I still have a really hard time reasoning about exception masking, so I’m hoping somebody can check my work here!
Can we be sure that close always runs?
Can we be sure that close cannot run twice?
And then, if we are confident that this code is correct (or if we can fix it), I’m wondering if there is an appropriate library that might welcome the addition of this function? (Or does it already exist somewhere?) I think masking is a really difficult subject, and so the more of these patterns we can get into libraries, the better, in my opinion.
forkIO never throws, so the onException part wasn’t necessary.
Now using forkIOWithUnmask instead of forkIO, because it seems likely that you always want the action to run with async exceptions unmasked, not for the action in the thread to inherit the masking state from the context in which bracketFork was used.
The revised function:
bracketFork open close action =
mask_ $
do
resource <- open
forkIOWithUnmask $ \unmask ->
unmask (action resource >> return ())
`finally`
close resource
Or, looking at the definition of finally, I think this would be equivalent and slightly more straightforward:
bracketFork open close action =
mask_ $
do
resource <- open
let a = action resource >> return ()
b = close resource >> return ()
forkIOWithUnmask $ \unmask ->
(unmask a `onException` b) >> b
Edit: Alternatively, this should be the same thing, written more clearly, I think.
{- | Like 'bracket', except the action and the cleanup
take place in a newly-forked thread. -}
bracketFork ::
IO resource -- ^ The first action, runs in the current thread
-> (resource -> IO a) -- ^ A final action that runs in the forked thread,
-- with async exceptions masked
-> (resource -> IO b) -- ^ Action that runs in the forked thread,
-- with async exceptions unmasked
-> IO ThreadId
bracketFork open close action =
mask_ $
do
resource <- open
action resource `forkUnmaskedFinally` close resource
{- | Like 'forkFinally', except the action always runs
with async exceptions unmasked. -}
forkUnmaskedFinally ::
IO a -- ^ Action that runs in the forked thread,
-- with async exceptions unmasked
-> IO b -- ^ A final action that runs in the forked thread,
-- with async exceptions masked
-> IO ThreadId
forkUnmaskedFinally action close =
mask_ $ forkIOWithUnmask $ \unmask ->
do
unmask action `onException` close
close
return ()