11. Concurrency and Threads

Computer does more than one thing at a time, for example while you are working on word processor your other application is downloading multiple files, media player is playing a song, editor is printing pages, and clock is displaying current time.

Even multiple tabs of single browser can download multiple websites at the same time. Such kind of application execution is called concurrency.

Computer has the ability to execute multiple programs (tasks or processes) at the same time using time slicing preemptive system. This is referred as multitasking or concurrency.

Multiple threads can be executed concurrently within a process is called multithreading.

In concurrent programming, there are two basic units of execution: processes and threads. A computer system normally has many active processes and threads.

Figure shows multiple processes and inside threads

Processes

A process has a self-contained execution environment. A process generally has a complete, private set of basic run-time resources; in particular, each process has its own memory space.

Processes cannot share its memory and other resources. Two processes can communicate with each other using environment variables.

Threads

Threads are sometimes called lightweight processes. Both processes and threads provide an execution environment, but creating a new thread requires fewer resources than creating a new process.

Threads are lightweight processes because they can share their allocated resources like memory, open files, open connections etc.

Threads exist within a process. Every process has at least one thread. Threads lying in the process, share resources of this process. Resources include memory and open files of the process. Multithreaded execution is an essential feature of Python platform. Every application has at least one thread.

Multithreading Vs Multiprocessing

Multithreading refers to an application with multiple threads running within a process. These threads are called ‘Light Weight Processes. Multithreading allows two parts of the same program to run concurrently. Concurrent threads belong to same process.

Multiprocessing is the execution of one or more processes by multiple processors inside one computer. They both work at the same time and execute concurrently by operating system. OS executes multiple processes concurrently using time slicing system.

Creating and starting thread

There are two ways to create a thread
  1. Create a function and start thread with help of Thread class.
  2. Create a class and inherit class by Thread class

Create thread by Functions

If you want to execute an operation concurrently in a thread then you create a function that is containing your operation. Operation is basically set of statements to be executed together.

For example here we want to print hello 5 times using a thread.

import threading
def say_hello():

    for i in range(5):
         print("Hello I am Ram")

t1 = threading.Thread(target = say_hello)
t1.start()

Thread object is created and this function will be executed when you start thread by calling its start() method.

Function with arguments

Functions may receive one or more parameters. You may also create a thread using parameterized functions.

Following function is receiving name parametre and print name 100 times:

import threading
def hello_thread(name):
     for i in range(100):
         print("Hello", name)

t1 = threading.Thread(target = hello_thread, args=('Ram',))
t2 = threading.Thread(target = hello_thread, args=('Shyam',))

t1.start()
t2.start()

Create thread by inheriting Thread class

First thing we need to do is to import Thread using the following code:

import threading

Next step is to create a new class name as MyThread which inherits threading.Thread class.

class MyThread(threading.Thread):


Here we seen MyThread inherits the threading.Thread class.

Now we define run() method in a subclass of threading.Thread where we pass self keyword inside this. This function will be executed when we call the start() method of that object in our MyThread class.

def run(self):
    pass

The next step is to create thread object of our thread class. then we call the start() method, that will execute the run() method of thread object.

Here we use for loop which will print the statement 10 times.

For Example:

import threading
class MyThread(threading.Thread):
    def run(self):
        print("Hello Ram")
        print("Hello Shyam")

for i in range(10):
     t = MyThread()
     t.start()

Note: we can't expect output

Thread Names

Thread name could be set or read using setName() and getName() methods.

In a function being called inside a thread, to get the current thread, use threading.current_thread(). In the following example we shall use this method to get current thread object.

To get the current thread Name

import threading

def first_thread():
     print(threading.current_thread().getName())


 t = threading.Thread(target=first_thread)

To set the Thread name we use setname() method in which we pass name of thread and start the thread by invoking start() method.

t.setName( 'Ram' )
t.start()

Thread Life Cycle

A thread has six lifecycle States, New, Runnable, Blocked, Waiting, Timed Waiting, and Terminated(Dead).

Figure: Lifecycle States of a thread


New Born State

When an instance of thread is created it enters into newly born state.Thread will not start its execution until its start() method is called.methods start() is called once in a life of thread. At this point, the thread is not considered alive

Runnable(ready-to-run)state

Once start() method is called goes to runnable state that has Ready Queue. Ready Queue contains threads waiting for CPU cycle . CPU cycle is allocated by the operating system on the basis of time slicing system this is called scheduling.

Blocked/Waiting State

A thread can enter in this state because of the following reasons:
  1. It is waiting for the resource that is currently used and locked by other thread.In other words threads is waiting for a monitor lock to get access on synchronized block/method that is currently used by other thread ...It is called blocked state.
  2. Thread is performing IO operations from file, network or hardware. It is called blocked state
  3. Thread is sleeping for given time. It is called timed-waiting.
  4. Thread is suspended for infinite time. It is called waiting state.

Dead/Terminated State

A thread can be considered dead when its run() method is finished or its stop() method is called. If any thread comes on this state that means it cannot ever run again.

Time Module

This module provides various time-related functions. For related functionality

sleep(sec)

causes the currently running thread to block for at least the specified number of milliseconds.

Key Instance Methods

The following methods are invoked on a particular thread object

run()

this method contains thread executions logics that performs custom operations.This method is invoked in runnable state.

start()

starts the thread in a separate path of execution, that invokes the run() method on this thread object

join(long millisec)

the current thread invokes this method on a second thread, causing the current thread to block until the second thread terminates or the specified number of milliseconds passes.

isAlive()

The isAlive() method checks whether a thread is still executing.i interrupt this thread, causing it to continue execution if it was blocked for any reason

getName()

The getName() method returns the name of a thread.setName(string name) −changes the name of the Thread object.

Key Static Methods

The Previous methods are invoked on a particular thread object. The following methods in the thread class are static. Invoking one of the static methods performs the operations on the currently running thread.

Method join() Vs wait()

Method wait() tells the calling thread to release the lock(monitor) and go to sleep until some other thread enters the same monitor and call notify()

If a thread call join() method to join some other thread then it does not release the lock but goes into waiting state until other thread finishes the job.Waiting thread than acquires the same lock and will continue processing. Suppose T1 thread is calling T2.join() then T1 will wait until T2 is finished then T1 will start back its processing.

Method wait(), notify() and notify_all()

Python includes an elegant communication mechanism among threads via the wait(), notify() and notify_all() methods. All three methods can be called only within a synchronized methods or blocks.
  1. wait() method releases the lock, and then blocks until it is awakened by a notify() or notifyAll() call for the same condition variable in another thread. Once awakened, it re-acquires the lock and returns. It is also possible to specify a timeout
  2. notify() method wakes up one of the threads waiting for the condition variable, if any are waiting.
  3. notifyAll() method wakes up all threads waiting for the condition variable. 

Daemon Thread

Daemon threads are service providers for other threads running in the same process as the non-daemon thread. Any python thread can be a daemon thread. Daemon threads are background supporting threads like Garbage Collector.

The run()method for a daemon thread is typically an infinite loop that waits for a service request. Any thread can be made daemon by calling daemon='true' method.

Usually daemon threads have higher priority than normal(non-daemon) threads.

Using daemon threads is useful for services where there may not be an easy way to interrupt the thread or where letting the thread die in the middle of its work without losing or corrupting data. 

Create Daemon Thread

Now Let's create two threads, one of them will take longer time to execute because we have added sleep of 2 seconds.
and  create a two thread objects and we will make Thread as a daemon thread by passing daemon=True as an argument in a thread function.

For Example:

import threading
import time

def first_thread():
    print('Hello Ram')
    time.sleep(2)
    print('Bye Ram ')

def second_thread():
     print('Hello Shyam')
     print('Bye Shyam ')

t1 = threading.Thread(target=first_thread, daemon=True)
t2 = threading.Thread(target=second_thread)

t1.start()
t2.start()

Now when you will run program you will see programs exits even though the daemon thread is running in the background.

Output:
Hello Ram
Hello Shyam
Bye Shyam

Race Condition

The situation where two threads compete for the same resource at the same time and order of accessing the resource is important, is called race condition.

In other words when two or more threads are concurrently modifying a single object is called race condition.

Threading module provides a Lock class to deal with the race conditions. Following are the methods:
  1. lock = threading.Lock() : This method is used to create lock object
  2. acquire() Method: This method is used to lock the current thread using lock.acquire(). 
  3. require() Method: This method is used to release the lock using lock.release() method.
Firstly we need to import the thread module
Let’s see the example to understand race condition

from time import sleep
from threading import *
class Account:
    balance =0
    def get_balance(self):
        sleep(2)
        return self.balance
    
    def set_balance(self, amount):
        sleep(2)
        self.balance = amount 

    def deposit(self, amount):
        bal = self.get_balance()
        self.set_balance(bal + amount)

#Create a class by inheriting Thread class

class Racing(Thread):
    account: Account
    name = "no name" 

    def __init__(self,account, name): #the method __init__ simulates the constructor of the class.
        super(Racing, self).__init__()
        self.account = account
        self.name = name

    def run(self):
        for i in range(5):
            self.account.deposit(100)
            print( self.name, self.account.get_balance())

    def main_task():         
        acc = Account() 
    
        #create thread instances
        t1 = Racing(acc, "Ram")
        t2 = Racing(acc, "Shyam")
       
        #Start threads
        t1.start()
        t2.start()
        t1.join()
        t2.join()
        print("Finish")

#Run main task
main_task()


Firstly we create a thread class name as Account and then define a global variable name as balance which has its value 0.
Next is we define the _init_ method with self keyword such as

Account is the class that is being accessed by two racing threads. Threads try to concurrently deposit amount 100 in the account .

Let’s suppose If concurrently two users try to deposit 100 rs each then both of them will call get_balance() at the same time and get the balance of 100 rs then set_balance() 200 rs at the same time. That show balance 200rs which is wrong , balance should be 300

Now we define the method as self.lock = threading.Lock() which is used to create the lock object .

Now next we define two functions names as get_balance() which returns the current balance and set_balance(self,amount) which returns the updated new balance amount.

Here above amount object is being accessed and updated by both the threads which leads to race condition.

To overcome this we use thread lock() method through which only one threads accessed at a time .

Now we define the deposit method which will be used to resolve race condition.

Now for race condition we call two methods first is self.lock.acquire which will lock the get_balance()function for sometime and when set_balance() update the new balance it will release the get_balance() by self.lock.release()

Now we create another class which will be inherited by thread class, and create multiple threads but only one access at a time. Inside this we create two object account and name object

Here we call super() class constructor which inherits from Thread class

When you run this program you will see every time the output will be different because every time the position of threads is changed in executio

synchronization

synchronization is the capability to control access of shared resources by concurrent multiple threads at the same time.

A synchronized shared resources can be accessed by only one thread at a time. In other words synchronized shared resource has only sequential access by concurrent threads.



                                                                Figure : Shared resource accessed by concurrent threads

For Example:
from time import sleep
from threading import *
import threading

class Account:
    balance =0
    def __init__(self):
    self.lock = threading.Lock()

     def get_balance(self):
         sleep(2)
         return self.balance

     def set_balance(self, amount):
         sleep(2)
         self.balance = amount

     def deposit(self, amount):
         self.lock.acquire()
         bal = self.get_balance()
         self.set_balance(bal + amount)
         self.lock.release()

#Create a class by inheriting Thread class

class Racing(Thread):
     account: Account
     name = "no name"
    
     def __init__(self,account, name): #the method __init__ simulates the constructor of the class.
         super(Racing, self).__init__()
         self.account = account
         self.name = name

    def run(self):
         for i in range(5):
             self.account.deposit(100)
             print( self.name, self.account.get_balance())

    def main_task():
        acc = Account()

     #create thread instances
         t1 = Racing(acc, "Ram")
         t2 = Racing(acc, "Shyam")

    #Start threads       
         t1.start()
         t2.start()
         t1.join()
         t2.join()
         print("Finish")

#Run main task

main_task()

join() Method

join() is used to join a thread with the main thread i.e. when join() is used for a particular thread the main thread will stop executing until the execution of joined thread is complete.

When join method is invoked, the calling thread is blocked till the thread object on which it was called is terminated. For example, when the join() is invoked from a main thread, the main thread waits until the other thread on which join is invoked exits.

The significance of join() method is, if join() is not invoked, the main thread may exit before the other thread, which will result undetermined behavior of programs and affect program invariant's and integrity of the data on which the program operates.

Once the threads start, the current program also keeps on executing. In order to stop execution of current program until a thread is complete, we use the join method.

First we create a thread function as first_thread by passing name as an argument and inside this we print Hello 20 times.

import threading
import time
def my_thread(name):
     for i in range(20):
         print('Hello ', name)

t1 = threading.Thread(target = my_thread, args=("Ram",))
t2 = threading.Thread(target = my_thread, args=("Shyam",))
t1.start()
t2.start()
t1.join()
t2.join()
print(“Bye”)

#So when you execute this above program it will print something like this see below output::

Output:

Hello Ram

Hello Ram

Hello Ram

Hello Ram

Hello Ram

Hello Shyam

Hello Shyam

Hello Shyam

Hello Shyam

etc….

Bye

Next again we create two objects of thread function names as t1 and t2 and here we can pass function to target argument and function parameters to args argument of thread constructor

Once the threads start, the current program like main thread also keeps on executing. In order to stop execution of current program until a two threads is complete, we use the join method.

Here we have joined two threads t1 and t2 which means that when threads t1 and t2 completes their execution then the main thread which is default thread will call and execute.

The Threading Module

The threading module exposes all the methods of the thread module and provides some additional methods:
  1. threading.activeCount() − Returns the number of thread objects that are active.
  2. threading.currentThread() − Returns the number of thread objects in the caller's thread control.
  3. threading.enumerate() − Returns a list of all thread objects that are currently active.
Let understand this with an example:

def first_thread():
     print("Hello I am Ram")

def second_thread():
     print("Hello I am Shyam")

#Thread function with arguments

def hello_thread(name):
    print("Hello", name)

t1 = threading.Thread(target = first_thread)
t2 = threading.Thread(target = second_thread)
t3 = threading.Thread(target = hello_thread, args=('Ajay',))
t4 = threading.Thread(target = hello_thread, args=('Vijay',))

t1.start()
t2.start()
t3.start()
t4.start()

# count active threads
print("Total number of threads", threading.activeCount())

#this will show the list of threads
print("List of threads: '' threading.enumerate())

Output:

Hello I am Ram

Hello I am Shyam

Hello Ajay

Hello Vijay

Total number of threads 2

List of threads: [<_MainThread(MainThread, started 10592)>]