Thread
Threaded programming is used by developers to improve the performance of
an application. This module shows how a multiplication operation with
some delay can be parallelized using ThreadPoolExecutor
.
A good grasp of threads is recommended, but not necessary, before reading the code below.
Here are some resources to learn more about threads:
https://realpython.com/intro-to-python-threading/ https://docs.python.org/3/library/threading.html
Python threads are not suitable for CPU-heavy tasks in the CPython interpreter due to the GIL. To address this, we typically resort to forking processes or running C externally.
Here are some resources to learn more about the GIL:
https://realpython.com/python-gil/ https://wiki.python.org/moin/GlobalInterpreterLock
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
# Module-level constants
_MULTIPLY_DELAY = 0.01 # delay is long enough for threads to be faster
def multiply_by_two(item):
"""This multiplication has a small delay."""
time.sleep(_MULTIPLY_DELAY)
return item * 2
def run_thread_workers(work, data):
"""Run thread workers that invoke work on each data element.
The inspiration for this function comes directly from an example
in the Python 3.x documentation:
https://docs.python.org/3/library/concurrent.futures.html
"""
results = set()
# We can use a with statement to ensure workers are cleaned up promptly
with ThreadPoolExecutor() as executor:
# Note that results are returned out of order
work_queue = (executor.submit(work, item) for item in data)
for future in as_completed(work_queue):
results.add(future.result())
return results
def main():
original_data = {num for num in range(5)}
expected_data = {(item * 2) for item in original_data}
# Let's get the data using the simple approach
simple_start = datetime.now()
simple_data = {multiply_by_two(item) for item in original_data}
simple_duration = datetime.now() - simple_start
# The simple approach has the expected data
assert simple_data == expected_data
# Let's get the data using the threaded approach
thread_start = datetime.now()
thread_data = run_thread_workers(multiply_by_two, original_data)
thread_duration = datetime.now() - thread_start
# The threaded approach has the expected data
assert thread_data == expected_data
# The threaded approach is faster than the simple approach in this case
# because a blocking I/O call like time.sleep forces the current thread
# to yield its control over to a waiting thread to start running. That
# means the threaded approach can run blocking operations in parallel.
# The cost of creating threads is somewhat cheap which means we often
# create more than one thread to speed up I/O-heavy workloads
assert thread_duration < simple_duration
if __name__ == "__main__":
main()