[More about concurrency Bryan O'Sullivan **20080528051111] { move ./examples/ch25/ModifyMVar_strict.hs ./examples/ch25/ModifyMVarStrict.hs addfile ./examples/ch25/Expensive.hs hunk ./en/ch25-concurrent.xml 264 - Safe resource management: a good idea, and easy besides + Safe resource management: a good idea, and easy + besides hunk ./en/ch25-concurrent.xml 401 + + + + Useful things to know about + + + MVar and Chan are non-strict + + Like most Haskell container types, both MVar + and Chan are non-strict: neither evaluates its + contents. We mention this not because it's a problem, but + because it's a common blind spot: people tend to assume that + these types are strict, perhaps because they're used in the + IO monad. + + As for other container types, the upshot of a mistaken + guess about the strictness of an MVar or + Chan type is often a space or performance leak. + Here's a plausible scenario to consider. + + We fork off a thread to perform some expensive computation + on another core. + + &Expensive.hs:notQuiteRight; + + It seems to do something, and puts + its result back into the MVar. + + &Expensive.hs:expensiveComputation; + + When we take the result from the MVar in the + parent thread and attempt to do something with it, our thread + starts computing furiously, because we never forced the + computation to actually occur in the other thread! + + As usual, the solution is straightforward, once we know + there's a potential for a problem: we add strictness to the + forked thread, to ensure that the computation occurs there. + This strictness is best added in one place, to avoid the + possibility that we might forget to add it. + + &ModifyMVarStrict.hs:modifyMVar_strict; + + + It's always worth checking Hackage + + In the Hackage package database, you will find a + library, strict-concurrency, that provides + strict versions of the MVar and + Chan types. + + hunk ./en/ch25-concurrent.xml 454 - + hunk ./en/ch25-concurrent.xml 459 - Chan. If one thread continually writes to the + Chan. If one thread writes to a hunk ./en/ch25-concurrent.xml 461 - it, the Chan will grow in an unchecked - manner. - + it, the Chan will grow in an unchecked manner: + unread messages will pile up as the reader falls further and + further behind. + + hunk ./en/ch25-concurrent.xml 467 - - Exercises + + Shared-state concurrency is still hard hunk ./en/ch25-concurrent.xml 470 - - - - The Chan type is implemented using - MVars. Use MVars to develop a - BoundedChan library. - - + Although Haskell has different primitives for sharing data + between threads than other languages, it still suffers from the + same fundamental problem: writing correct concurrent programs is + fiendishly difficult. Indeed, several pitfalls of concurrent + programming in other languages apply to Haskell. hunk ./en/ch25-concurrent.xml 476 - - - Your newBoundedChan function - should accept an Int parameter, limiting - the number of unread items that can be present in a - BoundedChan at once. - - + The first of these is deadlock, in + which two or more threads get stuck forever due to a clash over + access to shared resources. One classic way to make a + multithreaded program deadlock is to forget the order in which + we must acquire locks. This kind of bug is so common, it has a + name: lock order inversion. While Haskell + doesn't provide locks, the MVar type is prone to + the order inversion problem. Here's a simple example. hunk ./en/ch25-concurrent.xml 485 - - - If this limit is hit, a call to your - writeBoundedChan function must - block until a reader uses - readBoundedChan to consume a - value. - - - - + &LockHierarchy.hs:nestedModification; + + If we run this in &ghci;, it will usually&emdash;but not + always&emdash;print nothing, indicating that both threads have + gotten stuck. + + The problem with the nestedModification + function is easy to spot. In the first thread, we take the + MVar a, then + b. In the second, we take + b, then a. If the first + thread succeeds in taking a and the second + takes b, both threads will block: each tries + to take an MVar that the other has already emptied, + so neither can make progress. + + In real code, these kinds of inversion problems are much + more difficult to spot. The taking of MVars is + often spread across several functions in different files, making + visual inspection more tricky. Worse, these problems are often + intermittent, which makes them tricky to + even reproduce, never mind isolate and fix. + + Concurrent software is also prone to + starvation, in which one thread + hogs a shared resource, preventing another from + using it. It's easy to imagine how this might occur: one thread + calls modifyMVar with a body that executes + for 100 milliseconds, while another calls + modifyMVar on the same MVar + with a body that executes for 1 millisecond. The second thread + cannot make progress until the first puts a value back into the + MVar. + + The non-strict nature of the MVar type can + either exacerbate or cause a starvation problem. If we put a + thunk into an MVar that will be expensive to + evaluate, and take it out of the MVar in a thread + that otherwise looks like it ought to be + cheap, that thread could suddenly become computationally + expensive if it has to evaluate the thunk. This makes the + advice we gave in + particularly relevant. + + Fortunately, the APIs for concurrency that we have covered + here are by no means the end of the story. A more recent + addition to Haskell, Software Transactional Memory, is both + easier and safer to work with. We will discuss it in chapter + XXX. hunk ./en/ch25-concurrent.xml 537 - MVar and Chan are non-strict + Exercises hunk ./en/ch25-concurrent.xml 539 - Like most Haskell container types, both MVar - and Chan are non-strict: neither one evaluates its - contents. + + + + The Chan type is implemented using + MVars. Use MVars to develop a + BoundedChan library. + + + + + + Your newBoundedChan function + should accept an Int parameter, limiting the + number of unread items that can be present in a + BoundedChan at once. + + + + + + If this limit is hit, a call to your + writeBoundedChan function must block + until a reader uses readBoundedChan + to consume a value. + + + + + + Although we've already mentioned the existence of the + strict-concurrency package in the Hackage + repository, try developing your own, as a wrapper around + the built-in MVar type. Following classic + Haskell practice, make your library type safe, so that + users cannot accidentally mix uses of strict and + non-strict MVars. + + + hunk ./en/ch25-concurrent.xml 579 + hunk ./examples/ch25/Expensive.hs 1 +{-- snippet notQuiteRight --} +import Control.Concurrent + +notQuiteRight = do + mv <- newEmptyMVar + forkIO $ expensiveComputation_stricter mv + someOtherActivity + result <- takeMVar mv + print result +{-- /snippet notQuiteRight --} + +{-- snippet expensiveComputation --} +expensiveComputation mv = do + let a = "this is " + b = "not really " + c = "all that expensive" + putMVar mv (a ++ b ++ c) +{-- /snippet expensiveComputation --} + +someOtherActivity = return () }