© Stefania Loredana Nita and Marius Mihailescu 2017

Stefania Loredana Nita and Marius Mihailescu, Practical Concurrent Haskell, https://doi.org/10.1007/978-1-4842-2781-7_6

6. Cancellation

Stefania Loredana Nita and Marius Mihailescu1

(1)Bucharest, Romania

When working with interactive applications, a thread may need to break another thread execution in a particular situation. The following are some general examples.

  • In web applications , the client presses an Abort button. In this case, some activities need to be aborted; for example, a thread downloads something, or a thread runs script, or a thread renders a page.

  • An application with a thread that runs an interface, and another thread that does intense computations, needs to abort the computation thread if the client is changing the parameter from the user interface.

  • Usually, in server-based applications , the client has time to emit a demand before closing the connection. This approach prevents any remaining hanging connections that use many resources.

It is very important to decide if the targeted thread has the property to accept the cancellation , or if it is instantly closes when a particular situation occurs. This represents a compromise.

  • When a thread can choose, it raises a risk because the thread may not respond for a short time, or even worse, it could become permanently unresponsive. When threads are not responding, it could lead to hangings or deadlock, which is very bad from the client’s point of view.

  • When the cancellation is asynchronous, pieces of the program that change state must be kept safe from cancellation; otherwise, an update could be interrupted at an inappropriate time and could leave pieces of information in an improper state.

In reality, we have to choose between only implementing the first option and implementing both options. If the second option is preselected, protection of a crucial part leads to changing the choosing behavior during the crucial part.

In almost all imperative languages, the second option is not set as the default behavior because there are many parts where states are changed in a program. One major benefit of Haskell is that most code is functional, thus a condition could abort or safely suspend a thread, and then reload the thread without effect. In addition, specific to Haskell in a purely functional program, threads cannot ask for the cancellation conditions (due to the definition of a functional program itself), thus it cancels implicitly.

Hence, fully asynchronous cancellation represents the sensible implicit choice in Haskell. The only projection problem is to decide the way that cancellation is controlled by the program in the IO monad.

In this chapter, we talk mainly about asynchronous cancellation for threads, which is a sensible subject because it could occur at crucial moments in a program’s execution. The way that asynchronous cancellation is used in a program should be determined in the code in the IO monad.

Asynchronous Exceptions

A very useful function in exception handling is bracket from Control.Exception.

bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c

This function allows the programmer setting up exception handling to deallocate some resources or to apply tidy-up operations. If we call bracket a b c, then a represents the operation that assigns the resource, b represents the operation that frees the resource, and c represents the actual operation that works. The following is the definition of bracket.

bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
bracket ahead after current = do
  a <- ahead
  c <- current a `onException` after a
  after a
  return c

You already know that exceptions are a useful part of the IO monad, and the best practice in writing code with the IO monad is to make use of bracket, and then to gain and issue resources in a proper way. An important fact is that bracket should continue its job even if a thread is aborted. Thus, in this case, the cancellation should act as an exception. There is a huge distinction between exceptions and cancellation. For example, one is a situation in which an exception occurs when the user tries to open a file that does not exist anymore and an exception occurs when the user clicks an Abort button. In a second example, it is an asynchronous exception, because interruption was unexpected. The exceptions that occur when throw and throwIO are used are called synchronous exceptions.

If we want to use asynchronous exceptions, Haskell delivers a powerful primitive called throwTo that is throws exceptions from one thread to another thread.

throwTo :: Exception e => ThreadId -> e -> IO ()

Identical to synchronized exceptions, the argument for throwTo must be of type Exception. ThreadId is the output of a prior running of forkIO. It could represent a thread that is running, is blocked, or has ended. To show how to utilize throwTo, we will create a little program in which we will use the new Async type (defined next) to handle errors. In this program, the user is downloading web pages concurrently, and can quit the downloading pressing the Q key. The following is the code with explanations.

data Async a = Async (MVar (Either SomeException a)) -- 1

async :: IO a -> IO (Async a)
async something = do
  variable <- newEmptyMVar
  forkIO (do r <- try something; putMVar variable r) -- 2
  return (Async variable)


waitCatch :: Async a -> IO (Either SomeException a)  -- 3
waitCatch (Async var) = readMVar variable


wait :: Async a -> IO a                              -- 4
wait a = do
  x <- waitCatch a
  case x of
    Left b  -> throwIO b
    Right a -> return a
  • When MVar has Right a, the computation has successfully completed and returns the value a; when it has Left e, the computation throws exception e.

  • The action is included in try that outputs Either SomeException a —the necessary type for MVar.

  • There are two ways in which the result of Async could be waiting. First, waitCatch is returning Either SomeException a, which means the handling could be done right up. The second approach is to use wait, whose type is below. When wait is finding that in Async occurs an exception, it will throw again that exception.

wait :: Async a -> IO a

The preceding example is extended to permit cancellation. The following cancel operation is added.

cancel :: Async a -> IO ()

This function aborts an existing Async. If the operation is yet ended, then the function cancel has not any impact.

The implementation of cancel requires the ThreadId from the thread that runs the Async; thus, it needs to register it into Async type, together with MVar, which keeps the output. So, the Async type has the following definition.

data Async a = Async ThreadId (MVar (Either SomeException a))

Implement cancel as follows.

cancel :: Async a -> IO ()
cancel (Async t var) = throwTo t ThreadKilled

ThreadKilled belongs to Control.Exception. It is an exception that is thrown by cancel when a thread is called, even if it is already cancelled.

We now need to implement an async operation that will keep the output of the forkIO function from the Async constructor; namely, ThreadID.

async :: IO a -> IO (Async a)
async something = do
   mVar <- newEmptyMVar
   tVar <- forkIO (do r <- try something; putMVar mVar r)
   return (Async tVar mVar)

The following is an example (from Simon Marlow’s talk Parallel and Concurrent Haskell, also available here https://github.com/simonmar/parconcexamples/blob/master/geturlscancel.hs ) in which web pages are downloaded, but we want to also have the option to cancel the download.

sites = ["http://www.google.com",
         "http://www.bing.com",
         "http://www.yahoo.com",
         "http://www.wikipedia.com/wiki/Spade",
         "http://www.wikipedia.com/wiki/Shovel"]


main = do
  as <- mapM (async . timeDownload) sites                     -- 1


  forkIO $ do                                                 -- 2
     hSetBuffering stdin NoBuffering
     forever $ do
        c <- getChar
        when (c == 'q') $ mapM_ cancel as


  rs <- mapM waitCatch as                                     -- 3
  printf "%d/%d succeeded " (length (rights rs)) (length rs) -- 4

Let’s take a look at the code.

  1. Begin the download like in the initial example.

  2. Branch a thread that reads characters through standard input until it meets the Q character, in which case the cancel function is called for Asyncs.

  3. Expect the state of the output (completed or aborted).

  4. Display a report that mentions the number of successful operations. If the program is started and you press Q very quickly, the output should look like the following.

downloaded: http://www.google.com (14538 bytes, 0.17s)                
downloaded: http://www.bing.com (24740 bytes, 0.22s)
q2/5 finished

The program uses a long and complex HTTP library that does not supply assistance for cancellation and asynchronous I/O; Haskell provides modular cancellation assistance. A significant piece of code is not necessary for supporting it, even if some specifications should be taken into consideration when working with states.

Using Asynchronous Exceptions with mask

At the beginning of the previous section, we mentioned that a damaging cancellation could occur in the middle of an important action. For example, when an update process is suddenly interrupted, the data is left in an uncertain state, maybe forever. This is why you should supervise the way the asynchronous exceptions occur while running crucial sections. A solution could be the ability to postpone the asynchronous exception until the execution of crucial sections has finished; but this approach is not what we actually need.

Let’s imagine the following scenario: the takeMVar is called by a thread, which uses the value of MVar to make a computation, and then the output is stored by MVar. The implementation should allow asynchronous exceptions, but at the same time, it should be secure. For example, if an asynchronous exception occurs in the middle of a computation, then the value of MVar would be left empty. But this is not what should happen; instead, the initial value should be put back into MVar.

The following is a piece of code that illustrates that scenario. The function situation has two arguments: an m variable whose type is MVar, and an f function that makes a computation over the value of m in the IO monad.

situation :: MVar a -> (a -> IO a) -> IO ()
situation m f = do
  a <- takeMVar mVar                                 -- 1
  rVar <- f a `catch` e -> do putMVar mVar a; throw e  -- 2
  putMVar mVar rVar                                     -- 3

There are two places where MVar will be left without a value: from 1 to 2, and from 2 to 3. Actually, we could not intervene directly to be sure that the MVar value is not empty. But Haskell contains a great combinator called mask.

mask :: ((IO a -> IO a) -> IO b) -> IO b

The benefit of mask is that it delays the asynchronous exception so that its parameter has enough time to execute successfully. It is a little difficult, but the following is an example that shows how the combinator operates.

situation :: MVar a -> (a -> IO a) -> IO ()                
situation mVar f = mask $ estore -> do
  a <- takeMVar mVar
  rVar <- restore (f a) `catch` e -> do putMVar mVar a; throw e
  putMVar mVar rVar

The argument of mask is a function, which has the restore function as a parameter that brings an asynchronous exception to its actual state while the argument of mask is executing.

This described approach is useful because it brings a solution to our problem: exceptions can be raised only while (f a) is running, and there is a handler to catch every exception that occurs.

But we have not solved all of our problems, because the following is exposed: takeMVar is blocked for too much time and it is an inner mask combinator, thus the thread will not respond. Moreover, there is no relevant cause to hide exceptions while takeMVar is executing, rather the exception could occur almost until the stage in which takeMVar is returning.

On the other hand, in Haskell, by definition, the takeMVar should behave as described. Only a few operations would be interrupted while executing, including takeMVar, because takeMVar could block for an unknown time, and every operation that could block for an unknown time is designated as interruptible. These special types of operations could be interrupted by asynchronous exceptions, even if they are within a mask combinator.

Within mask, asynchronous exceptions are no longer asynchronous, but they could be activated by computations. Thus, asynchronous exceptions become synchronous exceptions inner mask combinator. The operations that could be interrupted are operations that could block for an undefined time. This is the expected behavior in almost all cases, as in our example.

Let’s return to the situation function. putMVar could be blocked for an undefined time, so it becomes interruptible, which leads us to the conclusion that problem is not safe yet, since an asynchronous exception could occur by blocking putMVar.

The property to be interrupted is very rigorously defined, so the situation function is OK. This is because an operation could be interrupted only if it really blocks; but this is not the case for putMVar because when it is called, the value of MVar is absolutely empty, so it will not be interrupted.

Are we sure that the MVar is really empty? Actually, we are not precisely sure, because another thread could call the putMvar function , applied on the same MVar after takeMVar from the problem function is performed. Still, we know that MVar is performed in a consistent manner, such that every call of takeMVar is succeeded by a putMvar. This represents a usual approach for a large number of MVar operations—a certain usage of MVar brings a protocol, in which operations should succeed or else there is the risk of deadlock.

Ultimately, there is a solution that permits a function to not be interrupted, and an asynchronous exception not to occur. The solution is uninterruptibleMask.

uninterruptibleMask :: ((IO a -> IO a) -> IO b) -> IO b

This is the same as mask, but it does not allow asynchronous exceptions to interrupt the interruptible functions. You need to pay attention to uninterruptibleMask, because the wrong use of it could produce significant damage, such that the application would no longer respond.

For debugging purposes, in the Control.Exception library there is a great function that allows us to verify if a current thread stands in the mask state or not.

getMaskingState :: IO MaskingState

data MaskingState
  = Unmasked
  | MaskedInterruptible
  | MaskedUninterruptible

As you can see, there are three constructors that could be returned by getMaskingState, one at a time, which are quite intuitive.

  • Unmasked. The targeted thread is not an inner mask or uninterruptibleMask.

  • MaskedInterruptible. The targeted thread is an inner mask.

  • MaskedUninterruptible. The targeted thread is an inner uninterruptibleMask.

There are higher-level combinators that exempt programmers from the necessity to use the mask combinator. As an example, the problem function from earlier is provided by the Control.Concurrent.MVar library, and its name is modifyMVar, because it is applicable when operating with MVars.

modifyMVar_ :: MVar a -> (a -> IO a) -> IO ()

Another variation of modifyMVar permits a different outcome among the novel content of the MVar.

modifyMVar :: MVar a -> (a -> IO (a, b)) -> IO b

The following is a simple example that implements the well-known compare-and-switch (CAS) operation .

compareAndSwitchMVar :: Eq a => MVar a -> a -> a -> IO Bool
compareAndSwitchMVar m oldValue newValue =
  modifyMVar m $ cur ->
    if cur == oldValue
       then return (newValue,True)
       else return (cur,False)

The parameters for compareAndSwitchMVar function are an MVar, a check valuable, and a new valuable. It works as follows: if the value of MVar is the same as the check value, then it changes with the new value and also returns True; else, the value of MVar is not changed and returns False.

If we want to work with more MVars, we should fit more calls of modifyMVar. The following modified the values of the two MVars in a safe way.

modifyTwoValues :: MVar a -> MVar b -> (a -> b -> IO (a,b)) -> IO ()
modifyTwoValues maValue mbValue f =
  modifyMVar_ mbValue $  ->
    modifyMVar maValue $ a -> f a b

When it is blocked inside modifyMVar, and an exception occurs, the content of MVar is restored to the initial value, with the help of an outside modifyMVar_.

Be careful with the order in which two or more values are taken. It should be the same order everywhere; otherwise, the program will have deadlock.

Extending the bracket Function

Let’s write a bracket function using mask, such that the function is safe if asynchronous exceptions occur.

bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
bracket ahead after something =
  mask $ estore -> do
    a <- ahead
    rVar <- restore (something a) `onException` after a
    _ <- after a
    return rVar

The IO actions are executed inside of mask, such as ahead and after. The bracket function assures that when ahead is returning, after is executed at a later time. The fact that ahead has a blocking process is legal. For example, if an exception occurs when ahead blocks, there is no damage. However, ahead could execute just a single blocking process. If a second blocking process raises an exception , it will not result in after being executed. If more blocking operations are necessary, then you should fit calls to bracket, as in the modifyMVar example.

Another thing to pay attention to is the use of blocking operations in after. If this is really necessary, be sure that the blocking operation can be interrupted and accept an asynchronous exception .

Writing Safe Channels Using Asynchronous Exceptions

Channels represent models for communications between processes, and synchronization between them through message passing (a method that invokes comportment on a computer, in which the programs are not called by names as in the classical method, but use object models to differentiate the general function from the particular implementations). Channels are useful because they facilitate the access of threads to the messages. Threads or processes can access a message that is sent through a channel if it has a reference to that channel.

In Haskell, channels belong to Control.Concurrent.Chan. The first step is to declare a channel, then to send data between threads using pipes, and then to extract data for different readers.

The following is a simple example in which file names are hashed and then printed using channels (see https://wiki.haskell.org/Haskell_for_multicores#Message_passing_channels ).

{-# LANGUAGE BangPatterns #-}
import Data.Digest.Pure.MD5
import qualified Data.ByteString.Lazy as L
import System.Environment
import Control.Concurrent
import Control.Concurrent.Chan
import Control.Monad (forever, forM_, replicateM_)


nrWorkers = 2

main = do
    files <- getArgs
    str <- newChan
    fileChan <- newChan
    forM_ [1..nrWorkers] (\_ -> forkIO $ worker str fileChan)
    forM_ files (writeChan fileChan)
    printNrResults (length files) str


printNrResults i var = replicateM_ i (readChan var >>= putStrLn)

worker :: Chan String -> Chan String -> IO ()
worker str fileChan = forever (readChan fileChan >>= hashAndPrint str)


hashAndPrint str f = do
        bs <- L.readFile f
        let !h = show $ md5 bs
        writeChan str (f ++ ": " ++ h)

In the remaing c ontent of section, there are examples that belong to Simon Marlow, from his talk “Parallel & Concurrent Haskell 3: Concurrent Haskell”, available at http://community.haskell.org/~simonmar/slides/cadarache2012/3%20-%20concurrent%20haskell.pdf . When working with MVars, we can make the code safer against asynchronous exception if we replace the use of takeMVar and putMVar with modifyMVar_. For example, the following function reads from a buffered channel, which is not safe when it deals with asynchronous exceptions.

readChan :: Chan a -> IO a
readChan (Chan readVar _) = do
  stream <- takeMVar readVar
  Item val new <- readMVar stream
  putMVar readVar new
  return val

When the execution of the primal takeMVar raises an asynchronous exception, the readMVar remains free, which leads to deadlock because of further read operations over Chan. modifyMVar helps achieve safer code.

readChan :: Chan a -> IO a
readChan (Chan readVar _) = do
  modifyMVar readVar $ stream -> do
    Item val tail <- readMVar stream
    return (tail, val)

The preceding modification is not sufficient. Do not forget that readMVar has the following definition.

readMVar :: MVar a -> IO a
readMVar m = do
  a <- takeMVar m
  putMVar m a
  return a

Looking at the readMVar definition, notice that when an exception occurs after the execution of takeMVar, but before the execution of readMVar, the content of the MVar is empty; therefore, it is necessary to assure the safety here, also. To do that, more options exist. One option is to utilize modifyMVar to reinstate the initial value. Another option is to utilize an extent of modifyMVar.

withMVar :: MVar a -> (a -> IO b) -> IO b

withMVar is similar with modifyMVar, but it does not modify the value of the MVar; thus, it is better for the goal of readMVar.

The easiest option is to only keep readMVar safe by using a mask. This approach is also adopted by the Control.Concurrent.MVar library.

readMVar :: MVar a -> IO a
readMVar m =
  mask_ $ do
    a <- takeMVar m
    putMVar m a
    return a

In the preceding implementation, mask_ is similar to mask; still, it does not have a restore function as a parameter. This implementation is enough because takeMVar and putMVar do not exist in any operation; thus, it is not needed in the exception handler.

The writeChan is a little more difficult. The initial definition is

writeChan :: Chan a -> a -> IO ()
writeChan (Chan _ writeVar) val = do
  newHole <- newEmptyMVar
  oldHole <- takeMVar writeVar
  putMVar oldHole (Item val newHole)
  putMVar writeVar newHole

To protect our code against exception, one idea is the following implementation.

wrongWriteChan :: Chan a -> a -> IO ()
wrongWriteChan (Chan _ writeVar) val = do
  newHole <- newEmptyMVar
  modifyMVar_ writeVar $ oldHole -> do
    putMVar oldHole (Item val newHole)  -- 1
    return newHole                      -- 2

Still, it is not safe, because in lines 1 and 2, an asynchronous exception could occur, which will let old_hole full and writeVarbounding to old_hole, which breaks the invariants of the data structure.

To resolve this last issue, all we need to do is apply mask_ over the entire sequence.

writeChan :: Chan a -> a -> IO ()
writeChan (Chan _ writeVar) val = do
  newHole <- newEmptyMVar
  mask_ $ do
    oldHole <- takeMVar writeVar
    putMVar oldHole (Item val newHole)
    putMVar writeVar newHole

Observe that both putMVars are ensured against blocking; thus, they cannot be interrupted.

timeout Variants

A function that limits the amount of time for an action to work is a popular approach in concurrent programming when using asynchronous exceptions. For this purpose in Haskell, there is timeout from the System.Timeout module wrapper. Its type is

timeout :: Int -> IO a -> IO (Maybe a)

timeout t m acts as follows.

  • Similar to fmap Just m, m returns an outcome or leads to an exception (inclusive to the asynchronous one) in t microseconds.

  • In other cases, it raises an exception in the format Timeout u. Timeout represents a novel data type, and u represents a single valuable whose type is Unique, so this specific instance of timeout is distinguished from any others. In this case, calling timeout gets Nothing.

In reality, the necessary t microsecond is approximated by timeout. In the first case, it is necessary that the execution of m be done under the conditions of the current thread, as m could need myThredId. It is also expected that m be interrupted by a thread that throws an exception to the current one, using throwTo. Having this anticipated behavior, nesting timeouts would help.

The following implementation for timeout is from the System.Timeout library (with some minor changes). It is a little complicated, but we will explain it. The main point is to branch a thread, which is waiting for t microseconds, until it calls throwTo for throwing the Timeout exception; thus, it is necessary that timeout terminate the thread until it returns.

timeout t m
    | t <  0    = fmap Just m                           -- 1
    | t == 0    = return Nothing                        -- 2
    | otherwise = do
        pid <- myThreadId                               -- 3
        u <- newUnique                                  -- 4
        let ex = Timeout u                              -- 5
        handleJust                                      -- 6
           (e -> if e == ex then Just () else Nothing) -- 7
           (\_ -> return Nothing)                       -- 8
           (bracket (forkIO $ do threadDelay t          -- 9
                                 throwTo pid ex)
                    ( id -> throwTo tid ThreadKilled)  -- 10
                    (\_ -> fmap Just m))                -- 11

Here are a few explanations.

  • Lines 1 and 2: Treat the simple cases of timeout being less or equal to zero.

  • Line 3: Get the ThreadId that belongs to the current thread.

  • Lines 4 and 5: newUnique generates a novel value, which creates a novel Timeout exception.

  • Line 6: handleJust handles exceptions and has the type

handleJust :: Exception e
           => (e -> Maybe b) -> (b -> IO a) -> IO a
           -> IO a
  • Line 7: The first parameter of handleJust says which exceptions are caught. For this example, we need to catch an exception of Timeout type that contains the unique valuable generated.

  • Line 8: The secondary parameter of handleJust handles the exceptions. In our case, it is returning Nothing because timeout takes place.

  • Line 9: This operation is running in handleJust. In this part, the novel thread is forked, making use of bracket, which assures that the novel thread is always terminated until the timeout function outputs. In the novel thread, threadDelay has a delay of t microseconds, and throwTo throws an exception whose type is Timeout to the initial thread.

  • Line 10: Terminates the youngest thread every time.

  • Line 11: The computation m (the secondary parameter of bracket) is running inside bracket, which will lead to the outcome Just.

We could check the correctness of the program in three cases.

  • m finishes and outputs a value.

  • The second thread (the new one) throws an exception; whereas m is still doing its tasks.

  • Both threads call throwTo at the same time.

The first two cases could be easily verified by running the program with specific arguments. The third case in the list is a little difficult. What happens in this case? It is actually conditioned by the implementation of throwTo. When bracket is called, it should not be allowed to output; whereas a Timeout exception could yet arise; this is necessary for timeout to work correctly. Therefore, when throwTo terminates the second thread, its call should be synchronous. In the event that it outputs, the second thread will not be able to throw an exception. This behavior is assured by the implementation of throwTo, which returns immediately after the exception occurs in the desired thread. As a consequence, throwTo could still be blocking when the second thread runs mask with asynchronous exceptions, which could be interrupted and get asynchronous exceptions.

For the third case, throwTo is called from both threads, which represents the expected and correct behavior.

Catching Asynchronous Exceptions

The behavior of an asynchronous exception is the same as a synchronous exception. It can be caught with catch or another function that catches exceptions from Control.Exception. Let’s say it was caught an asynchronous exception. We need to make some computations, but other asynchronous exception have occurred in the actual thread, which interrupts the initial function that handles exceptions. This behavior is not what we expect, because it could cause damage; the exception handlers could be interrupted by asynchronous exceptions.

The preceding behaviors can be avoided using mask and restore, as follows.

  mask $ 
estore ->
    restore action `catch` handler

We have already used this approach in some examples. The expected behavior is that asynchronous exceptions from mask stand inside the exception handler. Haskell brings a great benefit by doing this automatically, so there is no need to use mask explicitly. When returned from the handling exception, they are outside of mask again.

You need to pay attention and not remain in the default mask. The following is an example that shows this situation. The program inputs are file names by command line, and the output is represented by the number of lines for each file. If the file with a given name does not exist, then it is ignored.

main = do
  fs <- getArgs
  let
     loop !n [] = return n
     loop !n (f:fs)
        = handle (e -> if isDoesNotExistError e
                           then loop n fs
                           else throwIO e) $
            do
               getMaskingState >>= print
               h <- openFile f ReadMode
               s <- hGetContents h
               loop (n + length (lines s)) fs


  n <- loop 0 fs
  print n

The names of the files are read in the recursive loop function, where it tries opening and reading the content of each file, and saves the number of lines in the n variable. For every file, handle is called to assure exception handling. If isDoesNotExistError from System.IO.Error occurs, showing that the specified file does not exist, the exception handling is calling the recursive loop function to do the same operations over the remaining file names.

At first look, the program is working, but a problem could occur when getMaskingState is called. Next, the program runs with more file names that could not be found.

$ ./catch-mask xxx yyy
Unmasked
MaskedInterruptible
0

In the first iteration of the loop function , the state is Unmasked, which is good; but in the next iteration, it is reported that the current state is MaskedInterruptible, which is bad, because the synchronous exception should not be masked starting at the secondary iteration.

This situation has occurred because the loop function was called recursively by the exception handler, which means that the calling is done in the default mask from handle.

The following is the improved program.

main = do
  fs <- getArgs
  let
     loop !n [] = return n
     loop !n (f:fs) = do
        getMaskingState >>= print
        r <- Control.Exception.try (openFile f ReadMode)
        case r of
          Left e | isDoesNotExistError e -> loop n fs
                 | otherwise             -> throwIO e
          Right h -> do
            s <- hGetContents h
            loop (n + length (lines s)) fs


  n <- loop 0 fs
  print n

The loop function is not called any more recursively inside a mask. In addition, we have limited the purpose of the exception handler to only openFile, which is better than in the previous version.

You need to pay attention to this approach. In situations where handling asynchronous exceptions is needed, it is best that the exception handling is done within mask, so that current work is not interrupted by other asynchronous exception until it ends the first exception. This is why catch or handle are more suitable.

mask and forkIO Operations

Now, let’s return to the async function from the previous sections.

async :: IO a -> IO (Async a)
async action = do
   m <- newEmptyMVar
   t <- forkIO (do r <- try action; putMVar m r)
   return (Async t m)

This implementation is actually a mistake. When the Async is aborted, an exception is raised immediately after try; at putMVar, the thread terminates, but the content of MVar remains empty. The program experiences deadlock if it is waiting for the outcome of Async.

The code could be put inside a mask , but this will not help because, if an exception occurs before try, then the behavior will be similar. It follows a natural question: Which is the way in that asynchronous exceptions are put inside mask, in the place between creation of the thread and execution of try? Calling mask function within forkIO is not sufficient, because throw could be applied on an exception before calling mask.

This is why forkIO creates a thread that receives the state of mask as it is in the parent thread. So, it could create a thread that occurs in the masked state by encapsulating the forkIO call within the mask.

async :: IO a -> IO (Async a)
async action = do
   m <- newEmptyMVar
   t <- mask $ estore ->
          forkIO (do r <- try (restore action); putMVar m r)
   return (Async t m)

Another variation of forkIO is shown next. It allows you to take some action after a thread is completed.

forkFinally :: IO a -> (Either SomeException a -> IO ()) -> IO ThreadId
forkFinally action fun =
  mask $ estore ->
    forkIO (do r <- try (restore action); fun r)

Here is the improved version if the async function.

async :: IO a -> IO (Async a)
async action = do
   m <- newEmptyMVar
   t <- forkFinally action (putMVar m)
   return (Async t m)

Finally, the program is safe now.

Summary

In this chapter, you saw how asynchronous cancellation works for threads, and how to proceed when a thread can accept the cancellation, or is instantly aborted if a certain situation occurs. You also used useful operations such as bracket, timeout, mask, and forkIo. You were introduced to channels and you learned how to make them safe using asynchronous exceptions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.7.13