liveBook · Manning

Thread pool

As explained in Functional Design and Architecture: Examples in Haskell

A thread pool is a managed collection of reusable worker threads whose size limits concurrent thread execution.

A thread pool is a fixed-capacity resource pool for concurrent processes. Rather than letting business logic create threads freely, it exposes a restricted operation for starting a process and keeps thread creation in the application runtime. This makes unlimited nested forking unrepresentable and keeps concurrency limits separate from domain work. section 9.1.3

The motivating failure is unstructured use of `forkIO`: a loop can rapidly create vast numbers of threads and exhaust memory. A bounded pool instead permits only a predefined number of processes. Process logic may use shared state, random values, delays, logging, or database access, but it cannot fork child threads. section 9.1.3

astronomer :: ReportingChannel -> IO ()
astronomer ch = do
  someRef <- newIORef [1..10]
  forever $ forkIO $ do                   #1
    val <- readIORef someRef
    print val

This unbounded loop illustrates why raw thread creation must not be available throughout business logic. section 9.1.3

A layered design assigns distinct responsibilities:

  • `LangL` expresses domain actions such as delays, random-number generation, and synchronized shared-variable operations. section 9.1.3
  • `AppL` initializes state, coordinates processes, and provides the operation that forks a `LangL` process. section 9.1.3
  • The runtime owns the maximum thread count and current count, rather than exposing unrestricted thread control to domain code. section 9.1.4

Meteor collection under a process limit

To collect meteors in parallel, request a process for each `getRandomMeteor` action. `tryForkProcess` returns a token only when capacity is available; discard failed requests, then await each accepted token. This preserves a cap while allowing the caller to collect results from the processes that actually started. section 9.1.4 section 9.1.4

A token-and-join-style interface exposes important design problems. Awaiting a completed or destroyed process needs defined error behavior, and awaiting any process blocks until a result, failure, exception, or possibly an indefinite delay. Making the maximum mutable also creates race conditions: a limit can be lowered after another actor has already filled the pool. section 9.1.4 section 9.1.4

A simpler pool-facing operation blocks when the pool is full. Its interpreter synchronizes access to the current count through an `MVar`; when capacity is unavailable, it restores the count, delays with increasing backoff, and retries. When capacity exists, it forks the action and uses `forkFinally` to decrease the count after completion. section 9.1.4 section 9.1.4

Listing 9.4 Interpreter for LangL
interpretAppF :: Runtime -> AppF a -> IO a
  
interpretAppF rt (EvalLang act next) = do
  result <- runLangL rt act
  pure (next result)
  
interpretAppF rt (ForkProcess act next) =
  pure (next result)
  where
    result = go 1
    go factor = do                                    #1
      let psVar = _curThreadsCount rt
      let maxPs = _maxThreadsCount rt
  
      ps <- takeMVar psVar                            #2
  
      when (ps == maxPs) $ do                         #3
        putMVar psVar ps                              #4
        threadDelay (10 ^ factor)
        go (factor + 1)
  
      let ioAct :: IO () = runLangL rt act
      let finalAct _ = decreaseProcessCount psVar
      when (ps /= maxPs) $ do
        forkFinally ioAct finalAct                    #5
        putMVar psVar (ps + 1)
  
decreaseProcessCount :: MVar Int -> IO ()             #6
decreaseProcessCount psVar = do
  ps <- takeMVar psVar
  putMVar psVar (ps - 1)

The interpreter shows the pool boundary: synchronized counting, waiting at capacity, process startup, and count cleanup after completion. section 9.1.4

The practical boundary is architectural: domain scripts describe concurrent work, while the application layer controls when that work may consume a thread. Fixed limits avoid races caused by changing capacity during execution, though blocking at saturation remains an explicit behavior that callers must accept. section 9.1.3 section 9.1.4

How Functional Design and Architecture: Examples in Haskell covers it

How deep it goes
Works through restricted languages, process APIs, blocking behavior, token flaws, and an MVar-based interpreter.
Examples
Uses an unbounded `forkIO` loop and parallel meteor retrieval with process tokens.
What it assumes
Builds on free monadic languages and prior techniques for restricting raw IO.
What you can do after
Build it from scratch with fixed limits, synchronized counts, blocking admission, and completion cleanup.
How it connects
Connects thread pools with free monads, IO, MVar, race conditions, shared state, and process tokens.

Also explained in

Compare how 10 books explain thread pool →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Functional Design and Architecture: Examples in Haskell. The section links point to the parts of the book it draws on.