Multithreading in Python [With Coding Examples]

Improving and making code faster is the next step after the basic knowledge of Python is acquired. Multithreading is one such way to achieve that optimization using “Threads”. What are these threads? And how are these different from processes? Let’s find out.

By the end of this tutorial, you’ll have the knowledge of following:

  • What are threads and processes?
  • How Multithreading is achieved?
  • What are its limitations?
  • When to use Multithreading?

Threads in Python

When we think multitasking, we think parallel execution. Multithreading not strictly parallel execution. Threads can be thought of as separate entities of execution flow of different parts of your program running independently. So essentially, threads do not execute in parallel, but Python switches from one thread to another so fast that it seems they are parallel.

Processes on the other hand are strictly parallel and run on different cores to achieve execution faster. Threads can also be run on different processors, but they still won’t be running in parallel technically.

And are you thinking if threads don’t run parallely, then how do they make things faster? The answer is that they don’t always make processing faster. Multithreading is specifically used in tasks where threads will make processing faster.

All the information of a thread is contained in the Thread Control Block(TCB). TCB consists of the following main parts:

  1. A unique TID – Thread Identifier
  2. Stack Pointer which points to thread’s stack in the process 
  3. A Program counter which stores the address of the instruction currently being executed by the thread
  4. State of the Thread (running, ready, waiting, start or done)

Having said that, processes can contain multiple threads in it which share the code, data and all the files. And all the threads have their own separate register and stack which they have access to.

Now you might wonder, if the threads use the common data and code, how can they all use it without hampering other threads. This is the biggest limitation of Multithreading which we’ll talk about later in this tutorial.

Context Switching

Now as described above, threads don’t run parallely, but consequently. So when one thread T1 starts execution, all other threads remain in waiting mode. Only after T1 is done with its execution can any other queued thread begin to execute. Python switches from one thread to another so fast that it seems like parallel execution. This switching is what we call ‘Context Switching’.

Multithreaded programming

Consider below code which uses threads to perform a cube and a square operation.

 

import threading

def cuber(n):
print(“Cube: {}”.format(n * n * n))

def squarer(n):
print(“Square: {}”.format(n * n))

if __name__ == “__main__”:
# create the thread
t1 = threading.Thread(target=squarer, args=(5,))
t2 = threading.Thread(target=cuber, args=(5,))

# start the thread t1
t1.start()
# start the thread t2
t2.start()

# wait until t1 is completed
t1.join()
# wait until t2 is completed
t2.join()

# both threads completed
print(“Done!”

 

#Output:
Square: 25
Cube: 125
Done!

 

Now let’s try to understand the code.

First, we import the Threading module which is responsible for all the tasks. Inside the main, we create 2 threads by creating subclasses of the Thread class. We need to pass the target, which is the function that needs to be executed in that thread, and the arguments that need to be passed into those functions.

Now once the threads are declared, we need to start them. That is done by calling the start method on threads. Once started, the main program needs to wait for threads to finish their processing. We use the wait method to let the main program pause and wait for threads T1 and T2 finish their execution.

Must Read: Python Challenges for Beginners

Thread Synchronization

As we discussed above, threads do not execute in parallel, instead Python switches from one to another. So, there is a very critical need of correct synchronization between the threads to avoid any weird behavior. 

Race Condition

Threads which are under the same process use common data and files which can lead to a “Race” for the data between multiple threads. Therefore, if a piece of data is accessed by multiple threads, it will be modified by both the threads and the results we’ll get won’t be as expected. This is called a Race Condition.

So, if you have two threads which have access to the same data, then they both can access and modify it when that particular thread is executing. So when T1 starts executing and modifies some data, T2 is in sleep/wait mode. Then T1 stops execution and goes into sleep mode handing the control over to T2, which also has the access to the same data. So T2 will now modify and overwrite the same data which will lead to problems when T1 begins again.

The aim of Thread Synchronization is to make sure this Race Condition never comes and the critical section of code is accessed by threads one at a time in a synchronized way.

Locks

To solve and prevent the Race Condition and its consequences, the thread module offers a Lock class which uses Semaphores to help threads synchronise. Semaphores are nothing but binary flags. Consider them as the “Engaged” sign on Telephone booths which have the value as “Engaged” (equivalent to 1) or “Not Engaged” (Equivalent to 0). So everytime a thread comes across a segment of code with lock, it has to check if lock is already in 1 state. If it is, then it will have to wait until it becomes 0 so that it can use it.

The Lock class has two primary methods:

  1. acquire([blocking]): The acquire method takes in the parameter blocking as either True or False. If a lock for a thread T1 was initiated with blocking as True, it will wait or remain blocked until the critical section of code is locked by another thread T2. Once the other thread T2 releases the lock, thread T1 acquires the lock and returns True.

On the other hand, if the lock for thread T1 was initiated with parameter blocking as False, the thread T1 won’t wait or remain blocked if the critical section is already locked by thread T2. If it sees it as locked, it will straightaway return False and exit. However, if the code was not locked by another thread, it will acquire the lock and return True.

release(): When the release method is called on the lock, it will unlock the lock and return True. Also, it will check if any threads are waiting for lock to be released. If there are, then it will allow exactly one of them to access the lock.

However, if the lock is already unlocked, a ThreadError is raised.

Deadlocks

Another issue which arises when we deal with multiple locks is – Deadlocks. Deadlocks occur when locks are not released by threads due to various reasons. Let’s consider a simple example where we do the following:

import threading

l = threading.Lock()
# Before the 1st acquire
l.acquire()
# Before the 2nd acquire
l.acquire()
# Now acquired the lock twice

 In the above code, we call the acquire method twice but don’t release it after it is acquired for the first time. Hence, when Python sees the second acquire statement, it will go into the wait mode indefinitely as we never released the previous lock. 

These deadlock conditions might creep into your code without you realizing it. Even if you include a release call, your code may fail mid way and the release will never be called and the lock will stay locked. One way to overcome this is by using the withas statement, also called the Context Managers. Using the withas statement, the lock will get automatically released once the processing is over or failed due to any reason.

Read: Python Project Ideas & Topics

Before you go

As we discussed earlier, Multithreading is not useful in all applications as it doesn’t really make things run in parallel. But the main application of Multithreading is during I/O tasks where the CPU sits idly while waiting for data to be loaded. Multithreading plays a crucial role here as this idle time of CPU is utilized in other tasks, thereby making it ideal for optimization.

If you are curious to learn about python, data science, check out IIIT-B & upGrad’s PG Diploma in Data Science which is created for working professionals and offers 10+ case studies & projects, practical hands-on workshops, mentorship with industry experts, 1-on-1 with industry mentors, 400+ hours of learning and job assistance with top firms.

Prepare for a Career of the Future

UPGRAD AND IIIT-BANGALORE'S PG DIPLOMA IN DATA SCIENCE
Learn More

Leave a comment

Your email address will not be published. Required fields are marked *

×