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")