Python > Advanced Python Concepts > Concurrency and Parallelism > Choosing Between Threads and Processes
Thread Pool vs Process Pool Example
This snippet shows how to use `ThreadPoolExecutor` for I/O-bound operations and `ProcessPoolExecutor` for CPU-bound operations effectively.
Concepts Behind the Snippet
This example contrasts `ThreadPoolExecutor` and `ProcessPoolExecutor`. `ThreadPoolExecutor` is ideal for I/O-bound tasks where threads spend a significant amount of time waiting for external operations. `ProcessPoolExecutor`, on the other hand, is designed for CPU-bound tasks that benefit from true parallelism across multiple CPU cores.
Code: I/O-Bound Task with ThreadPoolExecutor
This code uses `ThreadPoolExecutor` to fetch the content length of several websites. Since fetching web pages is an I/O-bound task, threads can efficiently handle multiple requests concurrently. The `executor.map()` function applies the `fetch_url` function to each URL in the `urls` list, and the results are collected and printed. The `with` statement ensures that the thread pool is properly shut down after use.
import concurrent.futures
import time
import requests
urls = ['https://www.google.com', 'https://www.yahoo.com', 'https://www.bing.com', 'https://www.duckduckgo.com', 'https://www.wikipedia.org']
def fetch_url(url):
print(f'Fetching {url} in thread {threading.current_thread().name}')
response = requests.get(url)
return len(response.content)
if __name__ == '__main__':
start_time = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
results = executor.map(fetch_url, urls)
end_time = time.time()
print(f'ThreadPoolExecutor Execution Time: {end_time - start_time:.4f} seconds')
for url, length in zip(urls, results):
print(f'{url}: {length}')
Code: CPU-Bound Task with ProcessPoolExecutor
This code uses `ProcessPoolExecutor` to perform a CPU-bound calculation (summing squares) on a list of numbers. Each number is processed in a separate process, leveraging multiple CPU cores for parallel execution. `multiprocessing.cpu_count()` determines the number of CPU cores available, and the `max_workers` parameter of `ProcessPoolExecutor` is set accordingly. The `executor.map()` function applies the `cpu_bound_calculation` function to each number in the `numbers` list, and the results are collected and printed. The `with` statement ensures that the process pool is properly shut down after use.
import concurrent.futures
import time
import multiprocessing
def cpu_bound_calculation(n):
result = 0
for i in range(n):
result += i*i
return result
if __name__ == '__main__':
numbers = [10000000, 12000000, 11000000, 9000000, 13000000]
start_time = time.time()
with concurrent.futures.ProcessPoolExecutor(max_workers=multiprocessing.cpu_count()) as executor:
results = executor.map(cpu_bound_calculation, numbers)
end_time = time.time()
print(f'ProcessPoolExecutor Execution Time: {end_time - start_time:.4f} seconds')
for num, result in zip(numbers, results):
print(f'Calculation for {num}: {result}')
Real-Life Use Case
Best Practices
Interview Tip
Be prepared to discuss the use cases for `ThreadPoolExecutor` and `ProcessPoolExecutor`. Explain how the GIL affects the choice of executor for CPU-bound tasks. Also, be familiar with the `executor.map()` and `executor.submit()` methods.
When to Use Them
Memory Footprint
`ProcessPoolExecutor` generally has a higher memory footprint than `ThreadPoolExecutor` because each process has its own memory space.
Alternatives
Pros and Cons: ThreadPoolExecutor
Pros:
Cons:
Pros and Cons: ProcessPoolExecutor
Pros:
Cons:
FAQ
-
What is the difference between `executor.map()` and `executor.submit()`?
`executor.map()` applies a function to each item in an iterable and returns the results in the same order as the input. It blocks until all tasks are complete. `executor.submit()` submits a callable to the executor and returns a `Future` object, which represents the result of the asynchronous computation. You can then use the `Future` object to retrieve the result or check the status of the task. `executor.submit` is more flexible if you need to handle exceptions, manage dependencies, or retrieve results as they become available. -
How to choose the right `max_workers` for `ThreadPoolExecutor`?
The optimal `max_workers` value depends on the nature of the I/O-bound tasks. A larger number of workers can be beneficial if tasks spend most of their time waiting for external operations. However, too many workers can lead to increased overhead and contention for resources. Experimentation and profiling are often necessary to determine the optimal value.