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









