Python – Thread Pools

Introduction

Thread pools are a way to manage a group of threads for executing multiple tasks concurrently. Instead of creating a new thread for each task, a thread pool allows you to reuse a fixed number of threads. This can be more efficient and easier to manage. Python provides the concurrent.futures module, which includes the ThreadPoolExecutor class for managing thread pools.

Using ThreadPoolExecutor

The ThreadPoolExecutor class provides a high-level interface for asynchronously executing callables.

Basic Usage

Example

import concurrent.futures
import time

def task(name, duration):
    print(f"Task {name} starting")
    time.sleep(duration)
    print(f"Task {name} completed")
    return f"Task {name} result"

# Create a ThreadPoolExecutor
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
    # Submit tasks to the thread pool
    futures = [executor.submit(task, f"Task-{i+1}", i+1) for i in range(5)]

    # Wait for all tasks to complete and get results
    for future in concurrent.futures.as_completed(futures):
        print(future.result())

print("All tasks have finished execution.")

Output

Task Task-1 starting
Task Task-2 starting
Task Task-3 starting
Task Task-1 completed
Task Task-1 result
Task Task-4 starting
Task Task-2 completed
Task Task-2 result
Task Task-5 starting
Task Task-3 completed
Task Task-3 result
Task Task-4 completed
Task Task-4 result
Task Task-5 completed
Task Task-5 result
All tasks have finished execution.

Submitting Tasks with submit()

The submit() method schedules the callable to be executed and returns a Future object representing the execution of the callable.

Example

import concurrent.futures

def square(n):
    return n * n

# Create a ThreadPoolExecutor
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
    # Submit tasks to the thread pool
    future1 = executor.submit(square, 5)
    future2 = executor.submit(square, 10)

    # Get results
    result1 = future1.result()
    result2 = future2.result()

    print(f"Square of 5: {result1}")
    print(f"Square of 10: {result2}")

Output

Square of 5: 25
Square of 10: 100

Using map() Method

The map() method submits a callable to the thread pool for each item in the iterable and returns an iterator that yields the results.

Example

import concurrent.futures

def square(n):
    return n * n

numbers = [1, 2, 3, 4, 5]

# Create a ThreadPoolExecutor
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
    # Use map to execute tasks
    results = executor.map(square, numbers)

    # Print results
    for number, result in zip(numbers, results):
        print(f"Square of {number}: {result}")

Output

Square of 1: 1
Square of 2: 4
Square of 3: 9
Square of 4: 16
Square of 5: 25

Handling Exceptions

You can handle exceptions that occur during task execution by checking the Future object’s exception() method.

Example

import concurrent.futures

def divide(a, b):
    return a / b

# Create a ThreadPoolExecutor
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
    # Submit tasks to the thread pool
    future1 = executor.submit(divide, 10, 2)
    future2 = executor.submit(divide, 10, 0)

    # Get results and handle exceptions
    try:
        result1 = future1.result()
        print(f"10 / 2 = {result1}")
    except Exception as e:
        print(f"Exception occurred: {e}")

    try:
        result2 = future2.result()
        print(f"10 / 0 = {result2}")
    except Exception as e:
        print(f"Exception occurred: {e}")

Output

10 / 2 = 5.0
Exception occurred: division by zero

Waiting for Tasks to Complete

You can use as_completed() to wait for tasks to complete and retrieve their results as they become available.

Example

import concurrent.futures
import time

def task(name, duration):
    print(f"Task {name} starting")
    time.sleep(duration)
    print(f"Task {name} completed")
    return f"Task {name} result"

# Create a ThreadPoolExecutor
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
    # Submit tasks to the thread pool
    futures = [executor.submit(task, f"Task-{i+1}", i+1) for i in range(5)]

    # Wait for tasks to complete and get results
    for future in concurrent.futures.as_completed(futures):
        print(future.result())

print("All tasks have finished execution.")

Output

Task Task-1 starting
Task Task-2 starting
Task Task-3 starting
Task Task-1 completed
Task Task-1 result
Task Task-4 starting
Task Task-2 completed
Task Task-2 result
Task Task-5 starting
Task Task-3 completed
Task Task-3 result
Task Task-4 completed
Task Task-4 result
Task Task-5 completed
Task Task-5 result
All tasks have finished execution.

Conclusion

Thread pools in Python, managed by the ThreadPoolExecutor class in the concurrent.futures module, provide an efficient way to execute multiple tasks concurrently. By reusing a fixed number of threads, thread pools reduce the overhead of thread creation and destruction, making your programs more efficient and easier to manage. Understanding how to use submit(), map(), and handle exceptions with thread pools is essential for building robust and performant multithreaded applications in Python.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top