Using `mask`+`restore` across threads

The documentation of mask says:

To create a new thread in an unmasked state use forkIOWithUnmask.

Instead, couldn’t I use the restore function supplied by mask itself in the thread?

Concretely, is

\f -> mask_ $ forkIOWithUnmask $ \restore -> f restore

equivalent to

\f -> mask $ \restore -> forkIO $ f restore

?

It’s not.

It’s easier to see why when you assign the scoped function a more accurate name:

\f -> mask_ $ forkIOWithUnmask $ \unmask -> f unmask

vs

\f -> { *** } mask $ \restore -> forkIO $ f restore

In the first one, when unmask is called, it will unconditinally unmask asynchronous exceptions.

In the second one, when restore is called, it will restore the masking state from the point marked by ***. This means that if exceptions were already masked at this point by the outer mask that can’t be seen here, they will stay masked even after you use restore.

Here’s demonstration:

import Control.Concurrent
import Control.Exception
import System.Timeout

test1 :: IO ()
test1 = do
  uninterruptibleMask_ $ do
    sem <- newEmptyMVar
    _ <- mask_ $ forkIOWithUnmask $ \unmask -> do
      _ <- timeout 1_000_000 . unmask $ threadDelay 5_000_000
      putMVar sem ()
    readMVar sem

test2 :: IO ()
test2 = do
  uninterruptibleMask_ $ do
    sem <- newEmptyMVar
    mask $ \restore -> forkIO $ do
      _ <- timeout 1_000_000 . restore $ threadDelay 5_000_000
      putMVar sem ()
    readMVar sem

test1 takes 1 second, test2 takes 5.

3 Likes

Thanks! That clarifies that forkIOWithUnmask gives you an actual “unmask”, whereas mask only gives you a “restore”.