Multithreading in Python
Praveen kumar
Posted on July 18, 2023
Multi Threading
Hello folks!, today through this blog I will try to address a very important topic in any programming language that is multithreading . I will start with basics and take to you to the depth of the concept. I will be using notes style of explanation along with lot's of hands-on code snippets, which you can use to revise during your interview as well.
To understand multi-threading lets understand multi-tasking .
Multi-tasking is a process of executing multiple tasks simultaneously.
There are two types of multi-tasking:
Process based Multi-Tasking
Thread based Multi-Tasking
Process based Multi-Tasking
Executing several tasks simultaneously where each task is a separate independent process is called process based Multi-Tasking.
Example:
While typing notes in the web editor we are recording the session in the same system at same time.
Now at the same time we can download a file from internet as well. Again at the same we can play some music also. All these tasks are executing simultaneously and independent of each other. Hence this is the best example of multi-tasking.
This type of multi-tasking is suitable for OS level.
- Thread based Multi-Tasking:
Executing several tasks simultaneously where each task is a separate independent part of the same program, is called Thread based Multi Tasking, where each independent part is called a Thread.
This type of multi tasking is suitable at programmatic level.
The other name of Thread based Multi-Tasking is multithreading.
Note: Whether it is a process based or Thread based multi-tasking, the main advantage of multi-tasking is to improve the performance of the system by reducing the response time.
Some of the main important application areas of multithreading are:
To implement multimedia graphics.
To develop animations.
To develop video games.
To develop web and application servers .
etc
Note: Where ever a group of independent jobs are available, then it is highly recommended to execute simultaneously instead of executing one by one. For such type of cases we should go for multithreading.
Python provides one inbuilt module "threading" to provide support for developing threads. So python threading module makes our task easier to develop multi-threaded programs.
Every python program by default contains one thread which is called as Main Thread.
example:
#program to print the name of current executing thread
#importing the threading module
import threading
print("Current executing thread is: ", threading.current_thread().getName())
output:
py .\\main-thread.py
Current executing thread is: MainThread
Threading module contains a function current_thread() which returns the current executing Thread object. On this object if we call getName() method then we will get current executing thread name.
Different ways of Creating Thread in Python:
Majorly in python we can create thread in 3 ways:
Creating a Thread without using any class
Creating a Thread by extending a Thread class
Creating a Thread without extending Thread class.
Creating a Thread without using any class:
from threading import *
def display():
for i in range(1,11):
print("Child Thread")
#creating a Thread object explicitly
t = Thread(target=display)
#starting our thread by using start()
t.start()
for i in range(1,11):
print("Main Thread")
output:
py .\\thread-without-class.py
Child Thread
Child Thread
Child Thread
Main Thread
Main Thread
Child Thread
Main Thread
Child Thread
Main Thread
Child Thread
Main Thread
Child Thread
Main Thread
Main Thread
Main Thread
Main Thread
Child Thread
Main Thread
Child Thread
Child Thread
As shown in above program if multiple threads present in our program, then we cannot expect execution order and hence we cannot expect exact output for any multithreaded programs.
Due to the above reason we cannot provide exact output for the above program. The output is going to vary from machine to machine and run to run.
- Creating A Thread by extending Thread class:
For creating a thread by extending Thread class we need to create a child class for the Thread class.
In that child class we need to override run() method with our required job. Whenever we call start() method then automatically run() method will be executed and it will perform our job.
from threading import *
#implementing the child class for the parent Thread class
class MyThread(Thread):
def run(self):
for i in range(10):
print("Child Thread-1")
t = MyThread()
t.start()
for i in range(10):
print("Main Thread-1")
ouput:
py .\\extending-thread-class.py
Child Thread-1
Main Thread-1
Child Thread-1
Main Thread-1
Child Thread-1
Main Thread-1
Child Thread-1
Main Thread-1
Main Thread-1
Child Thread-1
Main Thread-1
Child Thread-1
Child Thread-1
Main Thread-1
Child Thread-1
Main Thread-1
Child Thread-1
Main Thread-1
Main Thread-1
- Creating a Thread without extending Thread class:
#Creating a thread by using class but not extending the Thread class
from threading import *
class WithoutThread:
def display(self):
for i in range(10):
print("Child Thread")
wt = WithoutThread()
t = Thread(target=wt.display)
t.start()
for i in range(10):
print("Main Thread!")
output
py .\\thread-without-extending-thread-class.py
Child Thread
Main Thread!
Child Thread
Main Thread!
Child Thread
Main Thread!
Child Thread
Main Thread!
Main Thread!
Child Thread
Main Thread!
Main Thread!
Child Thread
Child Thread
Main Thread!
Child Thread
Main Thread!
Child Thread
Child Thread
Main Thread!
writing a common program without multithreading to calculate squares and doubles of list items
#write a program to find double of a number and square of a number
import time
def doubles(nums):
for n in nums:
time.sleep(1)
print("Double of ",n," is :",(2*n))
def squares(nums):
for n in nums:
time.sleep(1)
print("Square of ", n, " is: ",(n*n))
nums = [1,2,3,4,5]
beginTime = time.time()
doubles(nums)
squares(nums)
print("the total time taken: ", (time.time() - beginTime))
ouput:
py .\\without-multithreading.py
Double of 1 is : 2
Double of 2 is : 4
Double of 3 is : 6
Double of 4 is : 8
Double of 5 is : 10
Square of 1 is: 1
Square of 2 is: 4
Square of 3 is: 9
Square of 4 is: 16
Square of 5 is: 25
the total time taken: 10.078748226165771
The same program if we write and run using multithreading then
#write a program to find double of a number and square of a number
from threading import *
import time
def doubles(nums):
for n in nums:
time.sleep(1)
print("Double of ",n," is :",(2*n))
def squares(nums):
for n in nums:
time.sleep(1)
print("Square of ", n, " is: ",(n*n))
nums = [1,2,3,4,5]
beginTime = time.time()
#creating first thread
t1 = Thread(target=doubles,args=(nums,))
t2 = Thread(target=squares,args=(nums,))
t1.start()
t2.start()
t1.join()
t2.join()
print("the total time taken: ", (time.time() - beginTime))
ouput:
py .\\with-multithreading.py
Double of 1 is : 2
Square of 1 is: 1
Square of 2 is: 4
Double of 2 is : 4
Double of 3 is : 6
Square of 3 is: 9
Double of 4 is : 8
Square of 4 is: 16
Double of 5 is : 10
Square of 5 is: 25
the total time taken: 5.035094738006592
Setting and Getting Name of a Thread:
Every thread in python has a name. It may be default name generated by Python Interpreter or Customized name provided by us (programmer).
NOw we can get or set the name of thread by using special methods of Thread class ie
t.getName() —> returns the name of Thread
t.setName(newName) —> To set our own name of a Thread.
Thead Identification Number(ident):
For every thread internally a unique identification number is available. we can access those ids by using implicit variable "ident".
from threading import *
print("current thread is: ", current_thread().getName())
current_thread().setName("First Thread")
print("current thread is: ", current_thread().getName())
print(current_thread().name)
#identification Number is a Thread
print(current_thread().ident)
py .\\threadName.py
current thread is: MainThread
current thread is: First Thread
First Thread
16404
example- 2
#identifying the threads with its identity number
from threading import *
def identity():
print("Child thread")
t = Thread(target=identity)
t.start()
print("Main thread identification number :", current_thread().ident)
print("Child thread identification number: ", t.ident)
Output
py .\\thread-identity.py
Child thread
Main thread identification number : 9548
Child thread identification number: 12348
active_count():
To count the number of active threads currently running we can use active_count() function of threading module
example:
#program to count the number of active threads using active_count() of threading module
from threading import *
import time
def display():
print(current_thread().getName(),"...started")
time.sleep(3)
print(current_thread().getName(),"...ended")
print("The number of active threads: ", active_count())
t1 = Thread(target=display,name="ChildThread-1")
t2 = Thread(target=display,name="ChildThread-2")
t3 = Thread(target=display,name="ChildThread-3")
t1.start()
t2.start()
t3.start()
print("The number of active threads are: ",active_count())
time.sleep(5)
print("The number of active threads are: ",active_count())
output:
py .\\active-thread.py
The number of active threads: 1
ChildThread-1 ...started
ChildThread-2 ...started
ChildThread-3 ...started
The number of active threads are: 4
ChildThread-2 ...ended
ChildThread-3 ...ended
ChildThread-1 ...ended
The number of active threads are: 1
enumerate() function:
This function returns a list of all active threads currently running
#using enumerate() of threading module
from threading import *
import time
#creating a funtion
def display():
print(current_thread().getName(),">>..started")
time.sleep(3)
print(current_thread().getName(),">>..ended")
#creating a thread
t1 = Thread(target=display,name="ChildThread1")
t2 = Thread(target=display,name="Childthread2")
t3 = Thread(target=display,name="ChildThread3")
t1.start()
t2.start()
t3.start()
thread_list = enumerate()
for thread in thread_list:
print("Thread Name:",thread.name)
time.sleep(5)
thread_list = enumerate()
for thread in thread_list:
print("Thread name is: ",thread.name)
py .\\thread-list.py
ChildThread1 >>..started
Childthread2 >>..started
ChildThread3 >>..started
Thread Name: MainThread
Thread Name: ChildThread1
Thread Name: Childthread2
Thread Name: ChildThread3
ChildThread3 >>..ended
Childthread2 >>..ended
ChildThread1 >>..ended
Thread name is: MainThread
isAlive():
This method is used to check whether a thread is still executing or not.
example:
#Checking whether a thread is alive or not
from threading import *
import time
def display():
print(current_thread().getName(),">>...started")
time.sleep(3)
print(current_thread().getName(),">>..ended")
t1 = Thread(target=display,name="childThread1")
t2 = Thread(target=display,name="childThread2")
t1.start()
t2.start()
print(t1.name,"is Alive",t1.is_alive())
print(t2.name,"is Alive",t2.is_alive())
time.sleep(5)
print(t1.name,"is Alive",t1.is_alive())
print(t2.name,"is Alive",t2.is_alive())
#task check whether the main thread is alive or not if alive when it dies
ouput
py .\\isAliveThread.py
childThread1 >>...started
childThread2 >>...started
childThread1 is Alive True
childThread2 is Alive True
childThread1 >>..ended
childThread2 >>..ended
childThread1 is Alive False
childThread2 is Alive False
join() method:
This method is used to wait until completion of some other thread. ie
the thread on which join() method will be called will be executed after the completion of other thread running currently.
#executing a thread after some thread process using join() method
from threading import *
import time
def display():
for i in range(10):
print("Snehal's Thread")
time.sleep(2)
t = Thread(target=display)
t.start()
#The blow line of code will be executed by Main Thread
t.join()
for i in range(10):
print("Main Thread")
Output:
py .\\joinThread.py
Snehal's Thread
Snehal's Thread
Snehal's Thread
Snehal's Thread
Snehal's Thread
Snehal's Thread
Snehal's Thread
Snehal's Thread
Snehal's Thread
Snehal's Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Note: We can call join() method with time period as well as shown below
t.join(seconds)
where t--> object reference on which we are calling the join(seconds) method
seconds--> time in seconds till which this thread will wait
example:
#executing a thread after some thread process using join() method
from threading import *
import time
def display():
for i in range(10):
print("Snehal's Thread")
time.sleep(2)
t = Thread(target=display)
t.start()
#The blow line of code will be executed by Main Thread
t.join(10)
for i in range(10):
print("Main Thread")
output:
py .\\joinThread.py
Snehal's Thread
Snehal's Thread
Snehal's Thread
Snehal's Thread
Snehal's Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Main Thread
Snehal's Thread
Snehal's Thread
Snehal's Thread
Snehal's Thread
Snehal's Thread
In above example clearly we can see that our Main Thread waited for 10 seconds as specified by the join(10) method after that it started executing the main thread.
Daemon Threads:
The thread which runs in the background are called Daemon Threads.
The main objective of the daemon threads is to provide support to non-daemon Threads(like main thread)
Eg: Garbage Collector
Whenever main Thread runs with low memory, immediately, PVM runs Garbage Collector to destroy useless objects and to provide free memory, so that Main Thread can continue its execution without having any memory problems.
How to check whether a given thread is Daemon thread ?
We can check for a Daemon thread in two ways:
By using isDaemon() method of Thread class
By using daemon property
from threading import *
print(current_thread().isDaemon())
print(current_thread().daemon)
output:
py .\\daemon.py
False
False
We can change the Daemon nature as well by using setDaemon() method of Thread class.
But wee can use this method before starting of Thread ie once thread started, we cannot change its Daemon nature, otherwise we will get as shown below:
from threading import *
print(current_thread().isDaemon())
print(current_thread().daemon)
print(current_thread().setDaemon(True))
ouput:
py .\\daemon.py
False
False
Traceback (most recent call last):
File "D:\\Software Training Classes\\Pythonier\\class-58\\daemon.py", line 4, in <module>
print(current_thread().setDaemon(True))
File "C:\\Users\\PRAVEEN\\AppData\\Local\\Programs\\Python\\Python39\\lib\\threading.py", line 1154, in setDaemon
self.daemon = daemonic
File "C:\\Users\\PRAVEEN\\AppData\\Local\\Programs\\Python\\Python39\\lib\\threading.py", line 1147, in daemon
raise RuntimeError("cannot set daemon status of active thread")
RuntimeError: cannot set daemon status of active thread
What is Default nature of a Thread?
By default Main Thread is always non-daemon.
But for the remaining threads Daemon nature will be inherited from parent to child ie
if the Parent Thread is Daemon the child Thread will also be daemon and vice-versa.
example:
#default nature of a thread
from threading import *
def job():
print("Child Thread")
t = Thread(target=job)
print("Daemon thread: ",t.isDaemon())
t.setDaemon(True)
print("Daemon Thread: ",t.isDaemon())
output
py .\\default-daemon.py
Daemon thread: False
Daemon Thread: True
Note: Main Thread is always Non-Daemon and we cannot change its Daemon Nature because it is already started at the beginning only.
Whenever The last Non-Daemon Thread is terminated automatically all Daemon Threads will be terminated.
Synchronization
If multiple threads are executing simultaneously then there may be a chance of data inconsistency problems as shown by the below program :
from threading import *
import time
def wish(name):
for i in range(10):
print("Good morning",end='')
time.sleep(2)
print(name)
t1 = Thread(target=wish,args=('Snehal',))
t2 = Thread(target=wish,args=('apurva',))
t1.start()
t2.start()
output:
py .\\synchronization.py
Good morningGood morningSnehal
apurva
Good morningGood morningapurva
Snehal
Good morningGood morningapurva
Snehal
Good morningGood morningapurva
Snehal
Good morningGood morningSnehal
apurva
Good morningGood morningapurva
Snehal
Good morningGood morningSnehal
apurva
Good morningGood morningSnehal
apurva
Good morningGood morningSnehal
apurva
Good morningGood morningSnehal
apurva
In above program we are getting irregular output because both the threads t1 and t2 are executing simultaneously wish() function. Now to solve the above problem we use Synchronization.
Synchronization is a process in which threads are executed one by one so that we can overcome the data inconsistency problems.
In other words synchronization means at a time only one thread
Some of the major application areas of synchronization are:
Online Reservation System.
Funds Transfer from joint accounts.
In python we can implement synchronization by using
Lock
RLock
Semaphore
Synchronization by using Lock concept
Lock concept is the most fundamental synchronization mechanism provided by the threading module in python.
We can create a Lock object as follows
lock1 = Lock()
The Lock object can hold only one thread at a time. If any other thread required at the same time to be executed then it will have to wait until thread releases the lock ( This is very similar to the concept of washroom, telephone booth etc)
How a Thread can acquire a lock?
A Thread can acquire the Lock by using the acquire() method. ie
lock1.acquire()
How a Thread which has acquired a Lock can release its Lock?
A Thread can release its Lock by using release() method.
lock1.release()
NOTE: On thing to note is to call release() method compulsory thread should be owner of that Lock. ie Thread should have Locked already, otherwise we will get RuntimeError: release unlocked lock
#to release a lock the thread should be already locked
from threading import *
lock1 = Lock()
#releasing a thread from the lock
lock1.release()
output:
py .\\Lock-release.py
Traceback (most recent call last):
File "D:\\Software Training Classes\\Pythonier\\class-59\\Lock-release.py", line 5, in <module>
lock1.release()
RuntimeError: release unlocked lock
Now to avoid the above error we need to lock the Lock() Thread first like shown below
#to release a lock the thread should be already locked
from threading import *
import time
lock1 = Lock()
#locking a thread
lock1.acquire()
print("The Thread is locked")
time.sleep(2)
#releasing a thread from the lock
lock1.release()
print("The thread is released from the lock!!")
Now we will not get any error as before
output
py .\\Lock-release.py
The Thread is locked
The thread is released from the lock!!
example 1: Synchronization using Lock concept
#synchronization using LOCK concept
from threading import *
import time
lock1 = Lock()
def wish(name):
lock1.acquire()
for i in range(10):
print("Good MOrning:",end='')
time.sleep(2)
print(name)
lock1.release()
t1 = Thread(target=wish,args=("Snehal",))
t2 = Thread(target=wish,args=("Python",))
t3 = Thread(target=wish,args=("Apurva",))
t1.start()
t2.start()
t3.start()
ouput:
py .\\synchronization-lock.py
Good MOrning:Snehal
Good MOrning:Snehal
Good MOrning:Snehal
Good MOrning:Snehal
Good MOrning:Snehal
Good MOrning:Snehal
Good MOrning:Snehal
Good MOrning:Snehal
Good MOrning:Snehal
Good MOrning:Snehal
Good MOrning:Python
Good MOrning:Python
Good MOrning:Python
Good MOrning:Python
Good MOrning:Python
Good MOrning:Python
Good MOrning:Python
Good MOrning:Python
Good MOrning:Python
Good MOrning:Python
Good MOrning:Apurva
Good MOrning:Apurva
Good MOrning:Apurva
Good MOrning:Apurva
Good MOrning:Apurva
Good MOrning:Apurva
Good MOrning:Apurva
Good MOrning:Apurva
Good MOrning:Apurva
Good MOrning:Apurva
In above program at a time only one thread is allowed to execute wish() function and hence we will get regular output without any confusion.
Problem with Simple Lock:
The standard Lock object doesn't care, which thread is currently holding that lock. If the lock is held and any other thread attempts to acquire that lock, then it will be blocked, even the same thread is already holding that lock.
#problem of simple thread Lock
from threading import *
lock1 = Lock()
print("Main Thread trying to acquire a Lock")
lock1.acquire()
print("Main Thread trying to acquire a Lock again..")
lock1.acquire()
output:
py .\\simple-thread-block.py
Main Thread trying to acquire a Lock
Main Thread trying to acquire a Lock again..
execution blocked
IN above program main thread will be blocked because it is trying to acquire the Lock for the second time.
NOte: To kill the blocking thread from the window command prompt we have to use ctrl+break.
If the Thread calls recursive functions or nested access to resources, then the thread may trying to acquire the same lock again and again, which may block our thread.
Hence Traditional Locking mechanism won't work for executing recursive functions. To overcome this problem, we should go for RLock(Re-entrant Lock).
Re-entrant means the thread can acquire the same lock again and again. If the lock is held by other threads then only the thread will be blocked. Re-entrant facility is available only for owner thread but not for other threads.
from threading import *
rlock = RLock()
print("Main Thread is acquire Lock")
rlock.acquire()
print("Main Thread trying to acquire the Lock again.")
rlock.acquire()
In the above case Main Thread won't be Locked because thread can acquire the lock any number of times. This RLock keeps track of recursion level and hence for every acquire() call compulsory release() call should be available. i.e the number of acquire() calls and release() calls should be matched then only lock will be released.
After 2 release() calls only the Lock will be released. Note:
Only owner thread can acquire the lock multiple times
The number of acquire() calls and release() calls should be matched.
Example: synchronization using RLock
from threading import *
import time
rlock = RLock()
#creating a factorial function
def factorial(n):
rlock.acquire()
if n == 0:
result = 1
else:
result = n*factorial(n-1)
rlock.release()
return result
def results(n):
print("the factorial of",n, "is ",factorial(n))
t1 = Thread(target=results,args=(5,))
t2 = Thread(target=results,args=(9,))
t1.start()
t2.start()
OUtput:
py .\\synchronization-rlock.py
the factorial of 5 is 120
the factorial of 9 is 362880
Difference between Lock and RLock
Lock RLock Lock object can be acquired by only one thread at a time. Even owner thread also cannot acquire multiple times. RLock object can be acquired by only one thread at a time, but owner thread can acquire same lock object multiple times Not suitable to execute recursive functions and nested access calls Best suitable to execute recursive functions and nested access calls In this case Lock object will takes care only Locked or unlocked and it never takes care about owner thread and recursion level. In this case RLock object will takes care whether Locked or unlocked and owner thread information, recursiion level
Synchronization by using Semaphore
In the case of Lock and RLock, at a time only one thread is allowed to execute. Sometimes our requirement is at a time a particular number of threads are allowed to access(like at a time 10 memebers are allowed to access database server,4 members are allowed to access Network connection etc).
To handle this requirement we cannot use Lock and RLock concepts instead we should use Semaphore concept. Semaphore can be used to limit the access to the shared resources with limited capacity. Semaphore is advanced Synchronization Mechanism. We can create a Semaphore object as shown below
Syntax:
sema = Semaphor(counter)
In the above syntax counter represents the maximum number of threads are allowed to access simultaneously. The default value of counter is 1. Whenever thread executes acquire() method, then the counter value will be decremented by 1 and if thread executes release() method then the counter value will be incremented by 1. i.e for every acquire() call counter value will be decremented and for every release() call counter value will be incremented.
Case-1: s=Semaphore() In this case counter value is 1 and at a time only one thread is allowed to access. It is exactly same as Lock concept.
Case-2: s=Semaphore(3) In this case Semaphore object can be accessed by 3 threads at a time.The remaining threads have to wait until releasing the semaphore.
example: Achieving synchronization using Semaphor
from threading import *
import time
sema = Semaphore(2)
def wish(name):
sema.acquire()
for i in range(10):
print("Good Evening: ",end= ' ')
time.sleep(2)
print(name)
sema.release()
t1 = Thread(target=wish,args=("Snehal",))
t2 = Thread(target=wish,args=("Apuva",))
t3 = Thread(target=wish,args=("Python",))
t4 = Thread(target=wish,args=("Postgresql",))
t5 = Thread(target=wish,args=("Django",))
t1.start()
t2.start()
t3.start()
t4.start()
t5.start()
Output:
py .\\sema-synchronous.py
Good Evening: Good Evening: Snehal
Apuva
Good Evening: Good Evening: Apuva
Snehal
Good Evening: Good Evening: Snehal
Apuva
Good Evening: Good Evening: Apuva
Snehal
Good Evening: Good Evening: Apuva
Snehal
Good Evening: Good Evening: Snehal
Apuva
Good Evening: Good Evening: Snehal
Apuva
Good Evening: Good Evening: Apuva
Snehal
Good Evening: Good Evening: Apuva
Snehal
Good Evening: Good Evening: Snehal
Apuva
Good Evening: Good Evening: Postgresql
Python
Good Evening: Good Evening: Postgresql
Python
Good Evening: Good Evening: Postgresql
Python
Good Evening: Good Evening: Postgresql
Python
Good Evening: Good Evening: Python
Postgresql
Good Evening: Good Evening: Python
Postgresql
Good Evening: Good Evening: Python
Postgresql
Good Evening: Good Evening: Python
Postgresql
Good Evening: Good Evening: Python
Postgresql
Good Evening: Good Evening: Python
Postgresql
Good Evening: Django
Good Evening: Django
Good Evening: Django
Good Evening: Django
Good Evening: Django
Good Evening: Django
Good Evening: Django
Good Evening: Django
Good Evening: Django
Good Evening: Django
IN the above program at a time 2 threads are allowed to access semaphore and hence 2 threads are allowed to execute wish() function simultaneously.
Note: Normally Semaphore is an unlimited semaphore which allows us to call release() method any number of times to increase the counter. The number of release() calls can exceed the number of acquire() calls also. ie
from threading import *
sema = Semaphore(2)
sema.acquire()
sema.acquire()
sema.release()
sema.release()
sema.release()
sema.release()
print("Ended")
Above program doesn't gives any error.
BoundedSemaphore:
Normal Semaphore is an unlimited semaphore which allows us to call release() method any number of times to increase the counter. The number of release() calls are exceeded in comparison to the number of acquire() calls also.
This mis-confusion is cleared by the BoundedSemaphore.
BoundedSemaphore is exactly same as Semaphore except that the number of release() method calls should not exceed the number of the acquire() method calls, otherwise we will get an error.
example:
#demo of boundedsemaphore
from threading import *
bs = BoundedSemaphore(2)
bs.acquire()
bs.acquire()
bs.release()
bs.release()
bs.release()
print("End")
output
py .\\boundedsemaphore.py
Traceback (most recent call last):
File "D:\\Software Training Classes\\Pythonier\\class-61\\boundedsemaphore.py", line 8, in <module>
bs.release()
File "C:\\Users\\PRAVEEN\\AppData\\Local\\Programs\\Python\\Python39\\lib\\threading.py", line 504, in release
raise ValueError("Semaphore released too many times")
ValueError: Semaphore released too many times
The above program is invalid because the number of release() method calls should be equal to number of acquire() method calls in BoundedSemaphore.
NOte: To prevent simple programming mistakes, IT is recommended to use BoundedSemaphore over normal Semaphore.
Difference Between the Lock and Semaphore
At a time Lock can be acquired by only one thread, but Semaphore object can be acquired by fixed number of threads specified by the counter value.
Conclusion:
The main advantage of synchronization is we can overcome data inconsistency problems. But the main disadvantage of synchronization is it increases waiting time of threads and creates performance problems. Hence if there is no specific requirements then it is not recommended to use synchronization.
Inter Thread Communication:
The concept in which threads are required to communicate with each other is called Inter Thread Communication.
Example: After producing items Producer thread has to communicate with Consumer thread to notify about the new item. Then only Consumer thread can consume the new item.
In Python language, we can implement inter thread communication by using different way as below:
Event
Condition
Queue
etc
Inter Thread Communication by using Event Objects:
Event object is the simplest communication mechanism between the threads.
In this one thread signals an event and other threads wait for it.
We can create Event object as shown below:
event = threading.Event()
Event object manages an internal flag that can set() or clear().
Other Threads can wait until event set.
Methods of Event class:
There are majorly four methods of Event class:
set(): internal flag value will become True and it represents GREEN signal for all waiting threading.
clear(): internal flag value will become False, which represents RED signal for all waiting threads.
isSet(): This method can be used to check whether an event it set or not.
wait() | wait(seconds) : Thread can wait untill event is set.
syntax for event:
event = threading.Event()
#to tell the consumer thread to wait untill event is set
event.wait()
#producer thread can set or clear the event
event.set()
event.clear()
Example:
from threading import *
import time
def producer():
time.sleep(5)
print("Producer thread producing items:")
print("Producer thread giving notification by setting the event.")
event.set()
def consumer():
print("Consumer thread is waiting for the updation:")
event.wait()
print("Consmer thread got notificaition and consuming the items.")
event = Event()
t1 = Thread(target=producer)
t2 = Thread(target=consumer)
t1.start()
t2.start()
output:
py .\\interThread-comm.py
Consumer thread is waiting for the updation:
Producer thread producing items:
Producer thread giving notification by setting the event.
Consmer thread got notificaition and consuming the items.
example-2
#Inter Thread Communication example
from threading import *
import time
def trafficpolice():
while True:
time.sleep(10)
print("Traffic Police Giving GREEN signal.")
event.set()
time.sleep(20)
print("Traffic Police giving red signal!!")
event.clear()
def driver():
num = 0
while True:
print("Driver waiting for the green signal:")
event.wait()
print("Traffic Signal turned Green.. Vechiles can start moving>>>")
while event.isSet():
num = num + 1
print("Vechile no",num,"Crossing the signal")
time.sleep(2)
print("Traffic Signal turned RED.. Drivers have to wait again..")
event = Event()
t1 = Thread(target=trafficpolice)
t2 = Thread(target=driver)
t1.start()
t2.start()
output:
py .\\trafffic-system-comm.py
Driver waiting for the green signal:
Traffic Police Giving GREEN signal.
Traffic Signal turned Green.. Vechiles can start moving>>>
Vechile no 1 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 2 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 3 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 4 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 5 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 6 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 7 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 8 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 9 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 10 Crossing the signal
Traffic Police giving red signal!!
Traffic Signal turned RED.. Drivers have to wait again..
Driver waiting for the green signal:
Traffic Police Giving GREEN signal.
Traffic Signal turned Green.. Vechiles can start moving>>>
Vechile no 11 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 12 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 13 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 14 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 15 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 16 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 17 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 18 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 19 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 20 Crossing the signal
Traffic Police giving red signal!!
Traffic Signal turned RED.. Drivers have to wait again..
Driver waiting for the green signal:
Traffic Police Giving GREEN signal.
Traffic Signal turned Green.. Vechiles can start moving>>>
Vechile no 21 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 22 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 23 Crossing the signal
Traffic Signal turned RED.. Drivers have to wait again..
Vechile no 24 Crossing the signal
IN the above program driver thread has to wait until Traffic police sets an event ie untill giving GREEN signal. Once Traffic signal thread sets event(giving Green signal), vehicles can cross the signal.
Once the traffic police thread clears event(giving RED signal) then the driver thread has to again wait.
Inter Thread Communication using Condition object:
We can say condition is more advanced version of Event object for inter-thread communication. A condition represents some kind of state change in the application like producing item or consuming item.
Thread can wait for that condition and threads can be notified once condition happened. ie Condition object allows one or more threads to wait until notified by another thread.
Condition is always associated with a lock (ReentrantLock).
A condition has acquire() and release() methods that call the corresponding methods of the associated lock.
We can create Condition object as given below:
condition = threading.Condition()
Methods of Condition object
Some of the important methods of condition object are given below:
acquire(): To acquire Condition object before producing or consuming items. ie thread acquiring the internal lock.
release(): To release Condition object after producing or consuming items. ie thread releases internal lock.
wait() | wait(time): To wait until getting notification or time expired.
notify() : To give notification for one waiting thread.
notifyAll(): To given notification for all waiting threads.
Case Study
The producing thread needs to acquire the Condition before producing an item to the resource and notifying the consumers.
#Producer Thread
...generate an item..
condition.acquire()
..add items to the resource..
condition.notify() # signal's that a new item is available(notifyAll())
condition.release()
The consumer must acquire the Condition and then it can consume items from the resource
#Consumer Thread
condition.acquire()
condition.wait()
consume item
condition.release()
Example:
#demo of condtional object
from threading import *
def consume(c):
c.acquire()
print("Consumer waiting for the updation...")
c.wait()
print("Consumer got notification and consuming the item..")
c.release()
#producer
def producer(c):
c.acquire()
print("Producer producing items")
print("Producer giving notification.")
c.notify()
c.release()
#creating an condition object
condition_obj = Condition()
t1 = Thread(target=consume,args=(condition_obj,))
t2 = Thread(target=producer,args=(condition_obj,))
t1.start()
t2.start()
output
py .\\condition-thread.py
Consumer waiting for the updation...
Producer producing items
Producer giving notification.
Consumer got notification and consuming the item..
Example-2
#condition thread object
from threading import *
import time
import random
#creating list of items
items = []
def produce(c):
while True:
c.acquire()
item = random.randint(1,100)
print("Producer producing item: ",item)
items.append(item)
print("Producer giving notification")
c.notify()
c.release() # it turns the condition to false
time.sleep(5)
def consume(c):
while True:
c.acquire()
print("Consumer waiting for the updation>>>")
c.wait()
print("Consumer got notification and consumed the item",items.pop())
c.release()
time.sleep(5)
# creating a condition object
condition_obj = Condition()
t1 = Thread(target=consume,args=(condition_obj,))
t2 = Thread(target=produce,args=(condition_obj,))
t1.start()
t2.start()
output
py .\\condition-thread2.py
Consumer waiting for the updation>>>
Producer producing item: 61
Producer giving notification
Consumer got notification and consumed the item 61
Producer producing item: 32
Producer giving notification
Consumer waiting for the updation>>>
Producer producing item: 18
Producer giving notification
Consumer got notification and consumed the item 18
Producer producing item: 41
Producer giving notification
Consumer waiting for the updation>>>
Producer producing item: 62
Producer giving notification
Consumer got notification and consumed the item 62
Producer producing item: 64
Producer giving notification
Consumer waiting for the updation>>>
Producer producing item: 95
Producer giving notification
Consumer got notification and consumed the item 95
Producer producing item: 88
Producer giving notification
Consumer waiting for the updation>>>
Producer producing item: 19
Producer giving notification
Consumer got notification and consumed the item 19
Producer producing item: 16
Producer giving notification
Consumer waiting for the updation>>>
Producer producing item: 24
Producer giving notification
Consumer got notification and consumed the item 24
Producer producing item: 66
Producer giving notification
Consumer waiting for the updation>>>
Producer producing item: 96
Producer giving notification
Consumer got notification and consumed the item 96
Producer producing item: 25
Producer giving notification
Consumer waiting for the updation>>>
Producer producing item: 53
Producer giving notification
Consumer got notification and consumed the item 53
Producer producing item: 46
Producer giving notification
Consumer waiting for the updation>>>
Producer producing item: 1
Producer giving notification
Consumer got notification and consumed the item 1
Producer producing item: 76
Producer giving notification
Consumer waiting for the updation>>>
In the above program consumer thread expecting updation and hence it is responsible to call wait() method on Condition object.
Producer thread performing updation and hence it is responsible to call notify() or notifyAll(() method on condition object.
Inter Thread Communication by using Queue: Queue concept is the most enhanced mechanism for inter thread communication and to share data between the threads.
Queue internally has Condition and that condition has Lock. Hence whenever we are using Queue we are not required to worry about Synchronization.
If we want to use Queue fist we should make use of queue module by importing it ie
import queue
After that we need to create queue object
ie
q = queue.Queue()
Important Methods of Queue:
put(): it is used to put an item into the queue.
get(): it is used to remove and return an item from the queue.
Producer Thread uses put() method to insert data in the queue. Internally this method has logic to acquire the lock before inserting the data into the queue. After inserting data the lock will be released automatically.
Here put() method also checks whether the queue is full or not and if queue is full then the Producer thread will entered in to waiting state by calling wait() method internally.
Cosumer Thread uses get() method to remove and get data from the queue. Internally this method has logic to acquire the lock before removing data from the queue. Once removal completed then the lock will be released automatically.
If the queue is empty then consumer thread will entered into waiting state by calling wait() method internally. Once queue updated with data then the thread will be notified automatically.
NOte: The queue module takes care of locking for us which is great advantage.
Example:
#making use of queue object
from threading import *
import time
import random
import queue
#defining a producer
def produce(q):
while True:
item = random.randint(1,100)
print("Producer producing item:", item)
q.put(item)
print("Producer giving notification")
time.sleep(5)
#defining a consumer
def consume(q):
while True:
print("Consumer waiting for updation:")
print("Consumer consumed an item:",q.get())
time.sleep(5)
#creating a queue object
qu_obj = queue.Queue()
t1 = Thread(target=consume,args=(qu_obj,))
t2 = Thread(target=produce,args=(qu_obj,))
t1.start()
t2.start()
output
py .\\queue-object.py
Consumer waiting for updation:
Producer producing item: 60
Producer giving notification
Consumer consumed an item: 60
Consumer waiting for updation:
Producer producing item: 49
Producer giving notification
Consumer consumed an item: 49
Consumer waiting for updation:
Producer producing item: 84
Producer giving notification
Consumer consumed an item: 84
Producer producing item: 49
Consumer waiting for updation:
Producer giving notification
Consumer consumed an item: 49
Producer producing item: 7
Producer giving notification
Consumer waiting for updation:
Consumer consumed an item: 7
Producer producing item: 66
Consumer waiting for updation:
Producer giving notification
Consumer consumed an item: 66
Producer producing item: 12
Consumer waiting for updation:
Producer giving notification
Consumer consumed an item: 12
Producer producing item: 58
Consumer waiting for updation:
Producer giving notification
Consumer consumed an item: 58
Producer producing item: 1
Types of Queues:
There are three types of Queues in Python:
- FIFO Queue| [First In First Out Queue]:
FIFO Queue is the default queue. In this type of queue in which order we put the items in the queue, in the same order the items will come out of it.
In general we create a FIFO queue object as shown below:
q = queue.Queue()
Example: FIFO Queue
#creating a FIFO queue
import queue
q = queue.Queue()
q.put(10)
q.put(5)
q.put(20)
q.put(15)
while not q.empty():
print(q.get(),end=' ')
output
10 5 20 15
- LIFO Queue | [Last In First Out]:
The queue in which the removal will be happened in the reverse order of insertion is known as LIFO Queue.
#creating a LIFO queue
import queue
#creating LIFO queue object
q = queue.LifoQueue()
q.put(5)
q.put(10)
q.put(15)
q.put(20)
while not q.empty():
print(q.get(),end=' ')
output
py .\\LIFO.py
20 15 10 5
- Priority Queue
A queue in which the elements will be inserted based on some priority order is known as Priority queue.
example:
#creating a Priority Queue object
import queue
q = queue.PriorityQueue()
q.put(5)
q.put(10)
q.put(15)
q.put(20)
while not q.empty():
print(q.get(),end=' ')
output
py .\\Priority.py
5 10 15 20
As we can see in above lines of code the Priority Queue is behaving as a FIFO queue.
case1: If the data is not numeric, then we have to provide our data in the form of tuple.ie
(x,y)→ where x is the priority and y is our element to be inserted.
example:
#creating a Priority Queue object
import queue
q = queue.PriorityQueue()
q.put((1,"apple"))
q.put((3,"ball"))
q.put((2,"bat"))
q.put((4,"cricket"))
while not q.empty():
print(q.get()[1],end=' ')
output
py .\\charac-Priority.py
apple bat ball cricket
In above code we saw that lowest is the value, highest will be it's priority.
Programming Practice with Locks
case-1
It is highly recommended to write code of releasing locks inside finally block.
The advantage is lock will be released always whether the exception raised or not raised and whether it handled or not handled. ie
loc = threading.Lock()
#acquiring a lok
loc.acquire()
try:
#performing some safe operation
finally:
#releasing the lock
loc.release()
example:
#we should must release lock in finally block
from threading import *
import time
loc = Lock()
def wish(name):
loc.acquire()
try:
for i in range(10):
print("Good Morning:",end=" ")
time.sleep(2)
print(name)
finally:
loc.release()
t1 = Thread(target=wish,args=("Snehal",))
t2 = Thread(target=wish,args=("Apurva",))
t3 = Thread(target=wish,args=("Python",))
t1.start()
t2.start()
t3.start()
Case-II It is highly recommended to acquire lock by using with statement.
The main advantage of with statement is the lock will be released automatically once control reaches end of the with block and we are not required to release the lock explicitly.
This is exactly same as usage of with statement for files. ie
with open('demo.txt','w') as text_file:
text_file.write("Hello this is my file")
In the same way in lock
lock = threading.Lock()
with lock:
#perform required safe operation
#lock will be released automatically
example:
#using lock with 'with' statement
from threading import *
import time
lock = Lock()
def wish(name):
with lock:
for i in range(5):
print("Good Evening: ",end=" ")
time.sleep(2)
print(name)
t1 = Thread(target=wish,args=("Snehal",))
t2 = Thread(target=wish,args=("Apurva",))
t3 = Thread(target=wish,args=("Python",))
t1.start()
t2.start()
t3.start()
Q. What is the advantage of using with statement to acquire a lock in threading?
Lock will be released automatically once control reaches end of the block and We are not required to release explicitly.
Note: We can use with statement in multithreading for the following cases:
Lock
RLock
Semaphore
Condition
That's the end of this blog folks! If you like the content, then do follow me for more content like those. Feel free to reach out to if you want any detailed tech docs on any topic of your interest. Till then bye bye !! keep coding keep learning.
Posted on July 18, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.