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 sleepclassbot1(Thread):defrun(self):for i inrange(2):print ("dog")sleep(1)classbot2(Thread):defrun(self):for i inrange(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*defprint_cube(num): """ function to print cube of given num """print("Cube: {}".format(num * num * num))defprint_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/python3from threading import*from time import*# Define a function for the threaddefprint_time( threadName,delay): count =0while count <2:sleep(delay) count +=1print ("%s: %s"% ( threadName, ctime(time()) ))# Create two threads as followstry: 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/python3from threading import*from time import*classmyThread (Thread):def__init__(self,threadID,name,counter): Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counterdefrun(self):print ("Starting "+ self.name)print_time(self.name, 5, self.counter)print ("Exiting "+ self.name)defprint_time(threadName,counter,delay):while counter:sleep(delay)print ("%s: %s"% (threadName, ctime(time()))) counter -=1# Create new threadsthread1 =myThread(1, "Thread-1", 1)thread2 =myThread(2, "Thread-2", 2)# Start new Threadsthread1.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 =0defincrement(): """ function to increment global variable x """global x x +=1defthread_task(): """ task for thread calls increment function 100000 times. """for _ inrange(100000):increment()defmain_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 inrange(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 =0defincrement(): """ function to increment global variable x """global x x +=1defthread_task(lock): """ task for thread calls increment function 100000 times. """for _ inrange(100000): lock.acquire()increment() lock.release()defmain_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 inrange(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/pythonfrom threading import*from time import*classmyThread (Thread):def__init__(self,threadID,name,counter): Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counterdefrun(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()defprint_time(threadName,delay,counter):while counter:sleep(delay)print ("%s: %s"% (threadName, ctime(time()))) counter -=1threadLock =Lock()# Create new threadsthread1 =myThread(1, "Thread-1", 1)thread2 =myThread(2, "Thread-2", 2)# Start new Threadsthread1.start()thread2.start()thread1.join()thread2.join()print ("Exiting Main Thread")