Concurrency is a crucial idea in modern programming that allows multiple tasks to run at the same time to improve the performance of applications.
There are several ways to achieve concurrency in Python, with threading and multiprocessing being the most well-known.
In this article, we'll explore these two methods in detail, understand how they work, and discuss when to use each, along with practical code examples.
What is Concurrency?
Before we talk about threading and multiprocessing, it’s important to understand what concurrency means.
Concurrency is when a program can do multiple tasks or processes at the same time.
This can make the program use resources better and run faster, especially when it needs to do things like reading files or doing lots of calculations.
There are two main ways to achieve concurrency:
- Parallelism: Running multiple tasks at the exact same time on different parts of the computer’s processor.
- Concurrency: Handling multiple tasks during the same time period, but not necessarily at the exact same moment.
Python offers two main ways to achieve concurrency:
- Threading: For tasks that can be managed at the same time.
- Multiprocessing: For tasks that need to run truly simultaneously on different processor cores.
Get the eBook
Inside, you'll discover a plethora of Python secrets that will guide you through a journey of learning how to write cleaner, faster, and more Pythonic code. Whether it's mastering data structures, understanding the nuances of object-oriented programming, or uncovering Python's hidden features, this ebook has something for everyone.
Threading in Python
Threading allows you to run multiple smaller units of a process, called threads, within the same process, sharing the same memory space.
Threads are lighter than processes, and switching between them is faster.
However, threading in Python is subject to the Global Interpreter Lock (GIL), which ensures only one thread can execute Python code at a time.
How Threading Works
Python's threading
module provides a simple and flexible way to create and manage threads.
Let’s start with a basic example:
import threading
import time
def print_numbers():
for i in range(5):
print(f"Number: {i}")
time.sleep(1)
# Creating a thread
thread = threading.Thread(target=print_numbers)
# Starting the thread
thread.start()
# Wait for the thread to complete
thread.join()
print("Thread has finished executing")
# Output:
# Number: 0
# Number: 1
# Number: 2
# Number: 3
# Number: 4
# Thread has finished executing
In this example:
- We define a function
print_numbers()
that prints numbers from 0 to 4 with a one-second delay between prints. - We create a thread using
threading.Thread()
and passprint_numbers()
as the target function. - The
start()
method begins the thread's execution, andjoin()
ensures that the main program waits for the thread to finish before proceeding.
Example: Threading for I/O-Bound Tasks
Threading is especially useful for I/O-bound tasks, such as file operations, network requests, or database queries, where the program spends most of its time waiting for external resources.
Here’s an example that simulates downloading files using threads:
import threading
import time
def download_file(file_name):
print(f"Starting download of {file_name}...")
time.sleep(2) # Simulate download time
print(f"Finished downloading {file_name}")
files = ["file1.zip", "file2.zip", "file3.zip"]
threads = []
# Create and start threads
for file in files:
thread = threading.Thread(target=download_file, args=(file,))
thread.start()
threads.append(thread)
# Ensure all threads have finished
for thread in threads:
thread.join()
print("All files have been downloaded.")
# Output:
# Starting download of file1.zip...
# Starting download of file2.zip...
# Starting download of file3.zip...
# Finished downloading file1.zip
# Finished downloading file2.zip
# Finished downloading file3.zip
# All files have been downloaded.
By creating and managing separate threads for each file download, the program can handle multiple tasks simultaneously, improving overall efficiency.
The key steps in the code are as follows:
- A function
download_file
is defined to simulate the downloading process. - A list of file names is created to represent the files that need to be downloaded.
- For each file in the list, a new thread is created with
download_file
as its target function. Each thread is started immediately after creation and added to a list of threads. - The main program waits for all threads to finish using the
join()
method, ensuring that the program does not proceed until all downloads are complete.
Limitations of Threading
While threading can improve performance for I/O-bound tasks, it has limitations:
- Global Interpreter Lock (GIL): The GIL restricts execution to one thread at a time for CPU-bound tasks, limiting the effectiveness of threading in multi-core processors.
- Race Conditions: Since threads share the same memory space, improper synchronization can lead to race conditions, where the outcome of a program depends on the timing of threads.
- Deadlocks: Threads waiting on each other to release resources can lead to deadlocks, where no progress is made.
Multiprocessing in Python
Multiprocessing addresses the limitations of threading by using separate processes instead of threads.
Each process has its own memory space and Python interpreter, allowing true parallelism on multi-core systems.
This makes multiprocessing ideal for tasks that require heavy computation.
How Multiprocessing Works
The multiprocessing module in Python allows you to create and manage processes easily.
Let’s start with a basic example:
This article is for subscribers only
To continue reading this article, just register your email and we will send you access.
Subscribe NowAlready have an account? Sign In