liveBook · Manning

Thread pool

As explained in Grokking Concurrency

A thread pool is a concurrency pattern that maintains a collection of reusable worker threads and assigns submitted tasks to available threads.

A thread pool is like a small crew already waiting for jobs. Instead of creating a new thread whenever work appears, a program starts a collection of long-running worker threads, gives them submitted tasks, and reuses them when each task finishes. The main thread can hand work to the pool rather than managing every worker itself. section 5.4 section 5.6

This arrangement avoids the cost of repeatedly creating threads and keeps a task failure, such as an exception, from ending the worker thread. It is especially useful when individual tasks take less time than creating a new thread would take. Pool management still has trade-offs: choosing the right number of workers depends on workload, system load, and hardware, and pools may use fixed or dynamically changing sizes. section 5.4

# Chapter 5/thread_pool.py
import time
import queue
import typing as T
from threading import Thread, current_thread

Callback = T.Callable[..., None]
Task = T.Tuple[Callback, T.Any, T.Any]
TaskQueue = queue.Queue

class Worker(Thread):
    def __init__(self, tasks: queue.Queue[Task]):
        super().__init__()
        self.tasks = tasks
 
    def run(self) -> None:
        while True:                                                    #1
            func, args, kargs = self.tasks.get()                       #1
            try:                                                       #1
                func(*args, **kargs)                                   #1
            except Exception as e:                                     #1
                print(e)                                               #1
            self.tasks.task_done()                                     #1
 
class ThreadPool:
    def __init__(self, num_threads: int):
        self.tasks: TaskQueue = queue.Queue(num_threads)               #2
        self.num_threads = num_threads
 
        for _ in range(self.num_threads):                              #3
            worker = Worker(self.tasks)                                #3
            worker.setDaemon(True)                                     #3
            worker.start()                                             #3
 
    def submit(self, func: Callback, *args, **kargs) -> None:
        self.tasks.put((func, args, kargs))
 
    def wait_completion(self) -> None:
        self.tasks.join()                                              #4
 
def cpu_waster(i: int) -> None:
    name = current_thread().getName()
    print(f"{name} doing {i} work")
    time.sleep(3)
 
def main() -> None:
    pool = ThreadPool(num_threads=5)                                   #5
    for i in range(20):                                                #6
        pool.submit(cpu_waster, i)                                     #6
 
    print("All work requests sent")
    pool.wait_completion()
    print("All work completed")
 
if __name__ == "__main__":
    main()

A compact Python implementation shows the basic loop: workers take functions from a queue, run them, handle exceptions, and mark tasks complete before accepting more work. section 5.4

Splitting a large search

To check many possible passwords, divide the search range into chunks and assign chunks to workers. Multiple chunks can run concurrently; an implementation creates workers based on available CPU cores and reports more than a threefold speedup over the earlier sequential version. section 5.5

A pool can also support asynchronous communication. An event loop can send a CPU-heavy job to a thread pool, immediately return a Future, and later receive notification that the result is ready. This keeps the caller’s execution thread free to continue other work while the job runs. section 12.10

How Grokking Concurrency covers it

How deep it goes
Defines worker reuse, task dispatch, exception isolation, pool sizing, and queue-based implementation, without detailing scheduling algorithms.
Examples
Uses password-search chunks and an asynchronous pizza server that returns a Future for background work.
What it assumes
Introduces pools through tasks and threads without assuming prior concurrency knowledge.
What you can do after
Build it from scratch by looping workers over queued tasks, then apply it to concurrent chunk processing or Future-based background jobs.
How it connects
Connects thread pools to task queues, Futures, event loops, asynchronous communication, concurrency, and parallelism.

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 Grokking Concurrency. The section links point to the parts of the book it draws on.