पाठ 18 / 25

Concurrency: Threads, MVar, STM and async

Write concurrent programs with lightweight threads and software transactional memory.

Concurrency without fear

GHC's runtime provides lightweight green threads: forkIO starts one cheaply, and thousands are normal. Compile with -threaded and run with +RTS -N to use all CPU cores. Communication options: MVar, a box that is either empty or full, usable as a lock or a one-slot channel; Chan for unbounded queues; and Software Transactional Memory (STM), Haskell's standout feature. With STM, you read and write **TVar**s inside an STM transaction and run it with atomically: transactions are composable, all-or-nothing and free of deadlocks from lock ordering. retry blocks a transaction until the variables it read change (perfect for "wait until there is enough stock"), and orElse composes alternatives. Purity makes STM practical: the type system guarantees that a transaction performs no irreversible I/O, so it can be safely re-run. The async library provides high-level tools: concurrently, race, mapConcurrently and withAsync, with proper exception propagation and cancellation. For parallel pure computation, the parallel package offers strategies such as parMap.

Bank transfers with STM and concurrent fetches with async

atomically, retry and mapConcurrently.

import Control.Concurrent.Async (mapConcurrently, concurrently)
import Control.Concurrent.STM
import Control.Concurrent (threadDelay)
import Control.Monad (when)

transfer :: TVar Int -> TVar Int -> Int -> STM ()
transfer from to amount = do
  balance <- readTVar from
  when (balance < amount) retry           -- block until 'from' has enough money
  modifyTVar' from (subtract amount)
  modifyTVar' to (+ amount)

fetchPrice :: String -> IO (String, Int)
fetchPrice sku = do
  threadDelay 100000                      -- simulate a 100 ms network call
  pure (sku, 4950)

main :: IO ()
main = do
  asha <- newTVarIO 1000
  ravi <- newTVarIO 0
  _ <- concurrently
         (atomically (transfer ravi asha 500))   -- waits until ravi has 500
         (atomically (transfer asha ravi 700))   -- runs first, then unblocks the other
  balances <- atomically ((,) <$> readTVar asha <*> readTVar ravi)
  print balances                                 -- (800,200)

  prices <- mapConcurrently fetchPrice ["pen", "ink", "pad"]   -- all three in parallel
  print prices

Keep IO out of transactions

STM transactions may be retried many times, so they must not perform I/O. The type system enforces this: you cannot call putStrLn inside STM, only in IO before or after atomically.

त्वरित जाँच: What does retry do inside an STM transaction?

  • Aborts the transaction and blocks until one of the TVars it read changes
  • Retries immediately in a busy loop
  • Throws an exception
  • Commits partially
Answer

Aborts the transaction and blocks until one of the TVars it read changes — retry waits for relevant state to change before re-running the transaction.