Starting with the basics of processes and threads, you’ll learn how multithreading works in Python—while understanding the concepts of concurrency and parallelism. You’ll then learn how to start and run one or more threads in Python using the built-in threading module. Let’s get started.
Processes vs. Threads: Differences
What Is a Process?
A process is any instance of a program that needs to run. It can be anything – a Python script or a web browser such as Chrome to a video-conferencing application. If you launch the Task Manager on your machine and navigate to Performance –> CPU, you’ll be able to see the processes and threads that are currently running on your CPU cores.
Understanding Processes and Threads
Internally, a process has a dedicated memory that stores the code and data corresponding to the process. A process consists of one or more threads. A thread is the smallest sequence of instructions that the operating system can execute, and it represents the flow of execution. Each thread has its own stack and registers but not a dedicated memory. All the threads associated with a process can access the data. Therefore, data and memory are shared by all the threads of a process. In a CPU with N cores, N processes can execute in parallel at the same instant of time. However, two threads of the same process can never execute in parallel- but can execute concurrently. We’ll address the concept of concurrency vs. parallelism in the next section. Based on what we’ve learned so far, let’s summarize the differences between a process and a thread.
Multithreading in Python
In Python, the Global Interpreter Lock (GIL) ensures that only one thread can acquire the lock and run at any point in time. All threads should acquire this lock to run. This ensures that only a single thread can be in execution—at any given point in time—and avoids simultaneous multithreading. For example, consider two threads, t1 and t2, of the same process. Because threads share the same data, when t1 is reading a particular value k, t2 may modify the same value k. This can lead to deadlocks and undesirable results. But only one of the threads can acquire the lock and run at any instant. Therefore, GIL also ensures thread safety. So how do we achieve multithreading capabilities in Python? To understand this, let’s discuss the concepts of concurrency and parallelism.
Concurrency vs. Parallelism: An Overview
Consider a CPU with more than one core. In the illustration below, the CPU has four cores. This means that we can have four different operations running in parallel at any given instant. If there are four processes, then each of the processes can run independently and simultaneously on each of the four cores. Let’s assume that each process has two threads. To understand how threading works, let us switch from multicore to single-core processor architecture. As mentioned, only a single thread can be active at a particular execution instance; but the processor core can switch between the threads. For example, I/O-bound threads often wait for I/O operations: reading in user input, database reads, and file operations. During this waiting time, I/O-bound thread can release the lock so that the other thread can run. The waiting time can also be a simple operation such as sleeping for n seconds. In summary: During wait operations, the thread releases the lock, enabling the processor core to switch to another thread. The earlier thread resumes execution after the waiting period is complete. This process, where the processor core switches between the threads concurrently, facilitates multithreading. ✅ If you want to implement process-level parallelism in your application, consider using multiprocessing instead.
Python Threading Module: First Steps
Python ships with a threading module that you can import into the Python script. To create a thread object in Python, you can use the Thread constructor: threading.Thread(…). This is the generic syntax that suffices for most threading implementations: Here,
target is the keyword argument denoting a Python callableargs is the tuple of arguments that the target takes in.
Define and Run Threads in Python
Let’s define a thread that runs a target function. The target function is some_func. Let’s parse what the above code snippet does:
It imports the threading and the time modules.The function some_func has descriptive print() statements and includes a sleep operation for two seconds: time.sleep(n) causes the function to sleep for n seconds.Next, we define a thread thread_1 with the target as some_func. threading.Thread(target=…) creates a thread object.Note: Specify the name of the function and not a function call; use some_func and not some_func().Creating a thread object does not start a thread; calling the start() method on the thread object does.To get the number of active threads, we use the active_count() function.
The Python script is running on the main thread, and we are creating another thread (thread1) to run the function some_func; so the active thread count is two, as seen in the output: If we take a closer look at the output, we see that upon starting thread1, the first print statement runs. But during the sleep operation, the processor switches to the main thread and prints out the number of active threads—without waiting for thread1 to finish executing.
Waiting for Threads to Finish Execution
If you want thread1 to finish the execution, you can call the join() method on it after starting the thread. Doing so will wait for thread1 to finish execution without switching to the main thread. Now, thread1 has finished executing before we print out the active thread count. So only the main thread is running, which means the active thread count is one. ✅
How to Run Multiple Threads in Python
Next, let’s create two threads to run two different functions. Here, count_down is a function that takes in a number as the argument and counts down from that number to zero. We define count_up, another Python function that counts from zero up to a given number. Next, we define two threads, thread1 and thread2 to run the functions count_down and count_up, respectively. We add print statements and sleep operations for both the functions. – To count down from a specific number to zero, you can use a negative step value of -1 and set the stop value to -1 so that zero is included. – Similarly, to count up to n, you have to set the stop value to n + 1. Because the default values of start and step are 0 and 1, respectively, you may use range(n + 1) to get the sequence 0 through n. When creating the thread objects, notice that the arguments to the target function should be specified as a tuple—to the args parameter. As both the functions (count_down and count_up) take in one argument, you’ll have to insert a comma explicitly after the value. This ensures the argument is still passed in as a tuple, as the subsequent elements are inferred as None. In the output:
The function count_up runs on thread2 and counts up to 5 starting at 0. The count_down function runs on thread1 counts down from 10 to 0.
You can see that thread1 and thread2 execute alternatively, as both of them involve a wait operation (sleep). Once the count_up function has finished counting up to 5, thread2 is no longer active. So we get the output corresponding to only thread1.
Summing Up
In this tutorial, you’ve learned how to use Python’s built-in threading module to implement multithreading. Here’s a summary of the key takeaways:
The Thread constructor can be used to create a thread object. Using threading.Thread(target=
You can code additional examples by tweaking the waiting times, trying for a different I/O operation, and more. Be sure to implement multithreading in your upcoming Python projects. Happy coding!🎉