repost: Introduction to
concurrent.futuresin Python - smrati katiyar
Concurrency in Python can be efficiently handled using the concurrent.futures module. This module provides a high-level interface for asynchronously executing function calls using either threads or processes. It allows you to run tasks in parallel, whether they are I/O-bound or CPU-bound, without worrying too much about the underlying details of thread and process management.
In this article, we’ll explore how to use concurrent.futures , explain the differences between threads and processes, and provide examples to illustrate the concepts.
# What is concurrent.futures ?
The concurrent.futures module provides two primary classes for parallel execution:
- ThreadPoolExecutor: Uses threads to perform tasks. Best suited for I/O-bound operations where tasks spend time waiting (e.g., file I/O or network requests).
- ProcessPoolExecutor: Uses separate processes to perform tasks. Ideal for CPU-bound tasks, such as computation-heavy operations, since Python’s Global Interpreter Lock (GIL) does not apply to separate processes.
Both classes work similarly and allow you to submit tasks for concurrent execution, providing a way to execute them in parallel or asynchronously while managing their results.
# Key Concepts
- Executor: An executor is an object that manages worker threads or processes.
- Future: A
Futureobject represents a result that may not have been computed yet. It allows you to check if the task is complete, retrieve the result, or cancel the task if needed. - submit: The
submit()method is used to schedule a function for execution and returns aFutureobject. - map: Similar to Python’s built-in
map(), but tasks are executed concurrently.
Let’s dive into examples to see how each component works.
# 1. Using ThreadPoolExecutor for I/O-Bound Tasks
The ThreadPoolExecutor is perfect for I/O-bound tasks like reading files, making network requests, or handling user input concurrently.
# Example: Downloading multiple web pages concurrently
1 | import concurrent.futures |
# Explanation:
- The
download_page()function fetches a webpage and returns the size of the page content. - Using
ThreadPoolExecutor, we submit the tasks to download each URL concurrently. - The
concurrent.futures.as_completed()function returns eachFutureas it completes, allowing us to process results as soon as they are ready.
# Output:
1 | https://www.python.org - 48730 bytes |
In this example, tasks that could take time to wait for network responses are handled in separate threads, speeding up the overall execution time.
# 2. Using ProcessPoolExecutor for CPU-Bound Tasks
For CPU-intensive tasks like mathematical computations, ProcessPoolExecutor is more suitable. Python’s GIL (Global Interpreter Lock) limits true parallelism in threads, but since processes have separate memory spaces, multiple processes can run in parallel on different CPU cores.
# Example: Parallel computation using ProcessPoolExecutor
1 | import concurrent.futures |
# Explanation:
- The
compute_factorial()function performs a CPU-bound operation of calculating the factorial of a number. - Using
ProcessPoolExecutor, we submit each factorial computation task to be executed in a separate process. - The
concurrent.futures.as_completed()function is used to retrieve results as they become available.
Output
1 | Factorial computed for number: 50000 |
This example demonstrates how CPU-bound tasks can be handled in parallel across multiple CPU cores, improving performance.
# 3. Using map for Simple Concurrent Task Execution
Both ThreadPoolExecutor and ProcessPoolExecutor provide a convenient method called map() to apply a function to multiple inputs concurrently. It works similarly to Python’s built-in map() , but with concurrent execution.
# Example: Using map with ThreadPoolExecutor
1 | import concurrent.futures |
Output
1 | [1, 4, 9, 16, 25] |
Here, executor.map() applies the square() function to each element in numbers concurrently. The map() function returns results in the same order as the input data, making it a convenient choice for cases where task order matters.
# 4. Handling Exceptions in Futures
When working with concurrent tasks, some tasks may fail or raise exceptions. concurrent.futures allows you to handle these exceptions gracefully.
# Example: Exception Handling with Future
1 | import concurrent.futures |
Output
1 | Result: 1 |
In this example, the faulty_task() function raises an exception for input 2 . The future.result() method either returns the result of the task or raises an exception if the task failed.
# Conclusion
The concurrent.futures module in Python offers a powerful and simple interface for writing concurrent and parallel code. Whether you’re dealing with I/O-bound tasks using ThreadPoolExecutor or CPU-bound tasks with ProcessPoolExecutor , this module provides a high-level way to manage tasks and retrieve results asynchronously.
By combining concepts like Future , submit() , and map() , you can easily implement concurrency into your Python programs and achieve significant performance improvements for both I/O-bound and CPU-bound operations.