Multithreading In Python

Master multithreading in python | 5 mins read

Multithreading is the key concept if you want to advance your skill in python. In this article , we will discuss why multithreading is so necessary. We will also compare multithreaded code and non-multi threaded code while solving a real coding scenario. In python, a thread is implemented as object of Thread class.

Table of Contents

84 / 100

Table of Contents

Introduction

Good to Go

Hello, and welcome back to another exciting tutorial.Today, we are going to talk about “Multithreading in Python“. This tutorial covers:-

  • Meaning of Thread
  • Meaning of Multithreading
  • Benefits of using multithreading
  • Implementation in python language
  • Examples regarding above topics for better understanding

If you aren’t following along, I would recommend you to go back where you left or start from here.

What is a thread?

Clear the basics

Threads are lightweight processes. They are termed so because they are smallest sequence of instruction that can be managed independently. A process can have multiple threads.

For example, while streaming a movie(a process), video and audio are two tasks which are handled by threads concurrently.

For more information regarding threads click here.

Why use Multithreading?

Clear the basics

Multithreading is effective in I/O tasks where the CPU remains idle because it needs other resources to be loaded. This idle time of CPU can be utilised in other tasks, thereby making it ideal for optimisation. Without multithreading:-
import time
import timeit
def task(name, tym):
print(f"Working on {name} , it will take {tym} secs.")
time.sleep(tym)
# Get time of before execution
start = timeit.default_timer()
task("Task1",5)
task("Task2",5)
# Get time of after execution
end = timeit.default_timer()
print('-----------------------------------------------------------')
print(f"Total time to execute the tasks is {end-start} secs.")
You can see that execution time is roughly 10 sec because both task function are executed sequentially.
Working on Task1 , it will take 5 secs.
Working on Task2 , it will take 5 secs.
-----------------------------------------------------------
Total time to execute the tasks is 10.010268292000001 secs.
With multithreading:-
import time
import timeit
import threading
def task(name, tym):
print(f"Working on {name} , it will take {tym} secs.")
time.sleep(tym)
# Get time of before execution
start = timeit.default_timer()
# Create threads
t1 = threading.Thread(target=task, args=('task1',5))
t2 = threading.Thread(target=task, args=('task2',5))
# Start first thread
t1.start()
# Start second thread
t2.start()
#  wait till the first thread is executed fully.
t1.join()
#  wait till the second thread is executed fully.
t2.join()
# Get time of after execution
end = timeit.default_timer()
print('-----------------------------------------------------------')
print(f"Total time to execute the tasks is {end-start} secs.")
print('Done')
You can see that execution time is roughly 5 sec because both task function are run on threads i.e concurrently.
Working on task1 , it will take 5 secs.
Working on task2 , it will take 5 secs.
-----------------------------------------------------------
Total time to execute the tasks is 5.072050499999932 secs.
Done

threading module in Python

Clear the basics

This module offers the core functionality to implement threading in python.

Nowadays , concurrent.futures module is more evidently used for multiprocessing and multithreading purpose.

Functions of threading module

  • threading.active_count() – Returns the number of active threads currently alive.
  • threading.enumerate() – Return a list of all thread objects that are currently alive..
  • threading.main_thread() – Returns the main. thread object.
  • threading.stack_size([size]) –Return the thread stack size when creating new threads.
import time
import threading
def task(name, tym):
time.sleep(tym)
t1 = threading.Thread(target=task, args=('task1',5))
t2 = threading.Thread(target=task, args=('task2',5))
t1.start()
t2.start()
print(f'Main thread is {threading.main_thread()}')
print('-----------------------------------------------------')
print(f'Active count of threads are {threading.activeCount()}')
print('-----------------------------------------------------')
print(f'All threads are {threading.enumerate()}')
print('-----------------------------------------------------')
print(f'Stack size {threading.stack_size()}')
print('-----------------------------------------------------')
t1.join()
t2.join()

Result

Main thread is <_MainThread(MainThread, started 8628913664)>
-----------------------------------------------------
Active count of threads are 11
-----------------------------------------------------
All threads are [<_MainThread(MainThread, started 8628913664)>, ......., <thread(thread-13, started="" 13146062848)="">]
-----------------------------------------------------
Stack size 0
-----------------------------------------------------
</thread(thread-13,>

Thread Class

The object of the thread class runs a function or process (passed in the constructor) in a separate thread

Methods of thread class
After thread object is created ,some methods are used to control its lifecycle.

  • start() – It starts the thread object and raise RuntimeError if called more than once on the same object.
  • join(timeout=None) – Wait till the thread object completes its execution.
    • The timeout accepts a float number (in secs) which specify the maximum time to complete the operation.

To check if timeout happens or join() function completed successfully, you should use is_alive() method on that thread.

  • run() – It represent the thread’s activity.
  • is_alive() – Return boolean value representing if thread is alive or not.
  • isDaemon() – Return boolean value representing if this thread is daemon(background process) or not.
import time
import timeit
import threading
def task(name, tym):
print(f"Working on {name} , it will take {tym} secs.")
time.sleep(tym)
# Get time of before execution
start = timeit.default_timer()
# Create threads
t1 = threading.Thread(target=task, args=('task1',5))
t2 = threading.Thread(target=task, args=('task2',5))
print('-------------Before start method-----------')
print('Is thread t1 alive =',t1.is_alive())
print('Is thread t2 alive =',t2.is_alive())
# Start first thread
t1.start()
# Start second thread
t2.start()
print('-------------Between start and join method-----------')
print('Is thread t1 alive =',t1.is_alive())
print('Is thread t2 alive =',t2.is_alive())
print('Is thread t1 a daemon =',t1.isDaemon())
#  wait till the first thread is executed fully.
t1.join()
#  wait till the second thread is executed fully.
t2.join()
print('-------------After join method-----------')
print('Is thread t1 alive =',t1.is_alive())
print('Is thread t2 alive =',t2.is_alive())
# Get time of after execution
end = timeit.default_timer()
print('-----------------------------------------------------------')
print(f"Total time to execute the tasks is {end-start} secs.")
Result
-------------Before start method-----------
Is thread t1 alive = False
Is thread t2 alive = False
Working on task1 , it will take 5 secs.
Working on task2 , it will take 5 secs.
-------------Between start and join method-----------
Is thread t1 alive = True
Is thread t2 alive = True
Is thread t1 a daemon = False
-------------After join method-----------
Is thread t1 alive = False
Is thread t2 alive = False
-----------------------------------------------------------
Total time to execute the tasks is 5.081889291999914 secs.

Real Code Scenario

The real experience

A good example would be web scraping. Suppose , you have a number of links and you have to perform some operation on each web page i.e get html text , find title of the page, get word density etc.

In our case, we have to check if web page is accessible or not.

import requests
import threading
def get_status_code(url):
req = requests.get(url)
if req.status_code == 200:
print(f'The {url} is accessible.')
else:
print(f'The {url} is not accessible.')
urls = ['https://topictrick.com/exception-handling-in-python/',
'https://topictrick.com/inheritance-in-python-and-operator-overloading/',
'https://topictrick.com/best-wifi-extender-under-50/',
'https://topictrick.com/bt-wifi-extender-1200-setup/',
'https://topictrick.com/wlan-repeater-setup/'
]
# Without using multi threading
for url in urls:
get_status_code(url)
# With using multi threading
for url in urls:
thread = threading.Thread(target = get_status_code,args = [url])
thread.start()

Here, I left you with a small task to compare the time duration for each strategy (without multithreading and with multithreading) and write your responses in the comment section.

Conclusion

Now , you are familiar with multi-threading in python, why it is necessary and what are the benefits of using it in program. Implement this knowledge in your code to make it quick and optimised.

Do leave your feedback and suggestions. We would love to hear your feedback.

Till then , keep coding.