Multithreading in Python

krpraveen0

Praveen kumar

Posted on July 18, 2023

Multithreading in Python

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:

  1. Process based Multi-Tasking

  2. Thread based Multi-Tasking

  3. 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.

  1. 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:

  1. To implement multimedia graphics.

  2. To develop animations.

  3. To develop video games.

  4. 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())
Enter fullscreen mode Exit fullscreen mode

output:

py .\\main-thread.py
Current executing thread is:  MainThread
Enter fullscreen mode Exit fullscreen mode

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:

  1. Creating a Thread without using any class

  2. Creating a Thread by extending a Thread class

  3. Creating a Thread without extending Thread class.

  4. 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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  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!")
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. t.getName() —> returns the name of Thread

  2. 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)
Enter fullscreen mode Exit fullscreen mode
py .\\threadName.py
current thread is:  MainThread
current thread is:  First Thread
First Thread
16404
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output

py .\\thread-identity.py
Child thread
Main thread identification number : 9548
Child thread identification number:  12348
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. By using isDaemon() method of Thread class

  2. By using daemon property

from threading import *
print(current_thread().isDaemon())
print(current_thread().daemon)
Enter fullscreen mode Exit fullscreen mode

output:

py .\\daemon.py
False
False
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

output

py .\\default-daemon.py
Daemon thread:  False
Daemon Thread:  True
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Online Reservation System.

  2. Funds Transfer from joint accounts.

In python we can implement synchronization by using

  1. Lock

  2. RLock

  3. 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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

How a Thread which has acquired a Lock can release its Lock?

A Thread can release its Lock by using release() method.

lock1.release()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!!")
Enter fullscreen mode Exit fullscreen mode

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!!
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

output:

py .\\simple-thread-block.py
Main Thread trying to acquire a Lock
Main Thread trying to acquire a Lock again..
execution blocked
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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:

  1. Only owner thread can acquire the lock multiple times

  2. 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()
Enter fullscreen mode Exit fullscreen mode

OUtput:

py .\\synchronization-rlock.py
the factorial of 5 is  120
the factorial of 9 is  362880
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Event

  2. Condition

  3. 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()
Enter fullscreen mode Exit fullscreen mode

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:

  1. set(): internal flag value will become True and it represents GREEN signal for all waiting threading.

  2. clear(): internal flag value will become False, which represents RED signal for all waiting threads.

  3. isSet(): This method can be used to check whether an event it set or not.

  4. 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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Methods of Condition object

Some of the important methods of condition object are given below:

  1. acquire(): To acquire Condition object before producing or consuming items. ie thread acquiring the internal lock.

  2. release(): To release Condition object after producing or consuming items. ie thread releases internal lock.

  3. wait() | wait(time): To wait until getting notification or time expired.

  4. notify() : To give notification for one waiting thread.

  5. 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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

output

py .\\condition-thread.py
Consumer waiting for the updation...
Producer producing items
Producer giving notification.
Consumer got notification and consuming the item..
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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>>>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

After that we need to create queue object

ie

q = queue.Queue()
Enter fullscreen mode Exit fullscreen mode

Important Methods of Queue:

  1. put(): it is used to put an item into the queue.

  2. 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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Types of Queues:

There are three types of Queues in Python:

  1. 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()
Enter fullscreen mode Exit fullscreen mode

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=' ')
Enter fullscreen mode Exit fullscreen mode

output

10 5 20 15
Enter fullscreen mode Exit fullscreen mode
  1. 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=' ')
Enter fullscreen mode Exit fullscreen mode

output

py .\\LIFO.py
20 15 10 5
Enter fullscreen mode Exit fullscreen mode
  1. 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=' ')
Enter fullscreen mode Exit fullscreen mode

output

py .\\Priority.py
5 10 15 20
Enter fullscreen mode Exit fullscreen mode

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=' ')
Enter fullscreen mode Exit fullscreen mode

output

py .\\charac-Priority.py
apple bat ball cricket
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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:

  1. Lock

  2. RLock

  3. Semaphore

  4. 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.

💖 💪 🙅 🚩
krpraveen0
Praveen kumar

Posted on July 18, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Multithreading in Python
python Multithreading in Python

July 18, 2023

Is Python’s asyncio Worth It?
python Is Python’s asyncio Worth It?

March 11, 2018