MultiThreading

MultiThreading :

This article covers the basics of multithreading in Python programming language. Just like multiprocessing, multithreading is a way of achieving multitasking. In multithreading, the concept of threads is used.

Let us first understand the concept of thread in computer architecture.

Thread

In computing, a process is an instance of a computer program that is being executed. Any process has 3 basic components:

  • An executable program.

  • The associated data needed by the program (variables, work space, buffers, etc.)

  • The execution context of the program (State of process)

A thread is an entity within a process that can be scheduled for execution. Also, it is the smallest unit of processing that can be performed in an OS (Operating System).

Example 1:

from threading import *
from time import sleep

class bot1(Thread):
    def run(self):
        for i in range(2):
            print ("dog")
            sleep(1)

class bot2(Thread):
    def run(self):
        for i in range(2):
            print ("cat")
            sleep(1)


b1 = bot1()
b2 = bot2()

b1.start()
b2.start()

b1.join()
b2.join()

print("byeye")

dog

cat

dog

cat

byeye

Example 2:

# Python program to illustrate the concept 
# of threading 
# importing the threading module 
from threading import *
  
def print_cube(num): 
    """ 
    function to print cube of given num 
    """
    print("Cube: {}".format(num * num * num)) 
  
def print_square(num): 
    """ 
    function to print square of given num 
    """
    print("Square: {}".format(num * num)) 
  

    # creating thread 
t1 = Thread(target=print_square, args=(10,)) 
t2 = Thread(target=print_cube, args=(10,)) 
  
    # starting thread 1 
t1.start() 
    # starting thread 2 
t2.start() 
  
    # wait until thread 1 is completely executed 
t1.join() 
    # wait until thread 2 is completely executed 
t2.join() 
  
    # both threads completely executed 
print("Done!")

Square: 100

Cube: 1000

Done!

#!/usr/bin/python3
from threading import *
from time import *
# Define a function for the thread
def print_time( threadName, delay):
    count = 0
    while count < 2:
        sleep(delay)
        count += 1
        print ("%s: %s" % ( threadName, ctime(time()) ))
# Create two threads as follows
try:
    t1 = Thread(target = print_time, args = ("Thread-1", 1,)) 
    t2 = Thread(target = print_time, args = ("Thread-2", 2,)) 
    t1.start() 
    
    t2.start()
    t1.join()
    t2.join()
    
    #_thread.start_new_thread( print_time, ("Thread-1", ) )
    #_thread.start_new_thread( print_time, ("Thread-2", ) )
except:
    print ("Error: unable to start thread")

Thread-1: Wed Apr 29 23:21:49 2020

Thread-2: Wed Apr 29 23:21:50 2020

Thread-1: Wed Apr 29 23:21:50 2020

Thread-2: Wed Apr 29 23:21:52 2020

Creating Thread Using Threading Module

#!/usr/bin/python3
from threading import *
from time import *

class myThread (Thread):
    def __init__(self, threadID, name, counter):
        Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter
    def run(self):
        print ("Starting " + self.name)
        print_time(self.name, 5, self.counter)
        print ("Exiting " + self.name)

def print_time(threadName, counter, delay):
    while counter:
        sleep(delay)
        print ("%s: %s" % (threadName, ctime(time())))
        counter -= 1

# Create new threads
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# Start new Threads
thread1.start()
thread2.start()

thread1.join()
thread2.join()

print ("Exiting Main Thread")

Starting Thread-1

Starting Thread-2

Thread-1: Tue May 21 00:11:48 2019

Thread-1: Tue May 21 00:11:49 2019

Thread-2: Tue May 21 00:11:50 2019

Thread-1: Tue May 21 00:11:50 2019

Thread-1: Tue May 21 00:11:51 2019

Thread-2: Tue May 21 00:11:52 2019

Thread-1: Tue May 21 00:11:52 2019

Exiting Thread-1

Thread-2: Tue May 21 00:11:54 2019

Thread-2: Tue May 21 00:11:56 2019

Thread-2: Tue May 21 00:11:58 2019

Exiting Thread-2

Exiting Main Thread

Synchronizing Threads :

Thread synchronization is defined as a mechanism which ensures that two or more concurrent threads do not simultaneously execute some particular program segment known as critical section.

Concurrent accesses to shared resource can lead to race condition.

A race condition occurs when two or more threads can access shared data and they try to change it at the same time. As a result, the values of variables may be unpredictable and vary depending on the timings of context switches of the processes.

Example:

import threading 

# global variable x 
x = 0

def increment(): 
	""" 
	function to increment global variable x 
	"""
	global x 
	x += 1

def thread_task(): 
	""" 
	task for thread 
	calls increment function 100000 times. 
	"""
	for _ in range(100000): 
		increment() 

def main_task(): 
	global x 
	# setting global variable x as 0 
	x = 0

	# creating threads 
	t1 = threading.Thread(target=thread_task) 
	t2 = threading.Thread(target=thread_task) 

	# start threads 
	t1.start() 
	t2.start() 

	# wait until threads finish their job 
	t1.join() 
	t2.join() 

if __name__ == "__main__": 
	for i in range(10): 
		main_task() 
		print("Iteration {0}: x = {1}".format(i,x)) 

Iteration 0: x = 175005

Iteration 1: x = 200000

Iteration 2: x = 200000

Iteration 3: x = 169432

Iteration 4: x = 153316

Iteration 5: x = 200000

Iteration 6: x = 167322

Iteration 7: x = 200000

Iteration 8: x = 169917

Iteration 9: x = 153589

In above program:

  • Two threads t1 and t2 are created in main_task function and global variable x is set to 0.

  • Each thread has a target function thread_task in which increment function is called 100000 times.

  • increment function will increment the global variable x by 1 in each call.

The expected final value of x is 200000 but what we get in 10 iterations of main_task function is some different values.

This happens due to concurrent access of threads to the shared variable x. This unpredictability in value of x is nothing but race condition.

Given below is a diagram which shows how can race condition occur in above program:

Notice that expected value of x in above diagram is 12 but due to race condition, it turns out to be 11! Hence, we need a tool for proper synchronization between multiple threads.

Using Locks

threading module provides a Lock class to deal with the race conditions. Lock is implemented using a Semaphore object provided by the Operating System.

Example:

import threading 

# global variable x 
x = 0

def increment(): 
	""" 
	function to increment global variable x 
	"""
	global x 
	x += 1

def thread_task(lock): 
	""" 
	task for thread 
	calls increment function 100000 times. 
	"""
	for _ in range(100000): 
		lock.acquire() 
		increment() 
		lock.release() 

def main_task(): 
	global x 
	# setting global variable x as 0 
	x = 0

	# creating a lock 
	lock = threading.Lock() 

	# creating threads 
	t1 = threading.Thread(target=thread_task, args=(lock,)) 
	t2 = threading.Thread(target=thread_task, args=(lock,)) 

	# start threads 
	t1.start() 
	t2.start() 

	# wait until threads finish their job 
	t1.join() 
	t2.join() 

if __name__ == "__main__": 
	for i in range(10): 
		main_task() 
		print("Iteration {0}: x = {1}".format(i,x)) 

Iteration 0: x = 200000

Iteration 1: x = 200000

Iteration 2: x = 200000

Iteration 3: x = 200000

Iteration 4: x = 200000

Iteration 5: x = 200000

Iteration 6: x = 200000

Iteration 7: x = 200000

Iteration 8: x = 200000

Iteration 9: x = 200000

Example:

#!/usr/bin/python

from threading import *
from time import *

class myThread (Thread):
        def __init__(self, threadID, name, counter):
            Thread.__init__(self)
            self.threadID = threadID
            self.name = name
            self.counter = counter
        def run(self):
            print ("Starting " + self.name)
            # Get lock to synchronize threads
            threadLock.acquire()
            print_time(self.name, self.counter, 3)
            # Free lock to release next thread
            threadLock.release()

def print_time(threadName, delay, counter):
    while counter:
        sleep(delay)
        print ("%s: %s" % (threadName, ctime(time())))
        counter -= 1
 
threadLock = Lock()

# Create new threads
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# Start new Threads
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print ("Exiting Main Thread")

Starting Thread-1

Starting Thread-2

Thread-1: Thu Apr 30 21:15:40 2020

Thread-1: Thu Apr 30 21:15:41 2020

Thread-1: Thu Apr 30 21:15:42 2020

Thread-2: Thu Apr 30 21:15:44 2020

Thread-2: Thu Apr 30 21:15:46 2020

Thread-2: Thu Apr 30 21:15:48 2020

Exiting Main Thread

Last updated