Multithreading in Python
Main
Posted on March 27, 2024
Normally, instructions in a computer program are executed in a sequential way, the execution of one task has to be complete before the next one begins. In some cases, however, we may want some tasks to be executed simultaneously or in overlapping periods of time, multi-threading is one way that we can achieve this quickly and efficiently. In computer programming terms, handling multiple tasks at the same time is referred to as concurrency, and therefore, multi-threading is just one way that a program can achieve concurrency.
Before we dive deeper onto how threads and multithreading works, you first need to understand two terms i.e process and thread.
In Python, a process refers to an instance of a program which is being executed. The operating system can handle multiple processes at the same time while ensuring data-integrity so that one process cannot access another process's data. Each process is allocated its own memory space where it stores and manipulates its data.
A thread, also known as a sub-process, refers to the smallest set of instructions that can be scheduled to be run by the processor. Threads are lightweight compared to processes, a single process can contain multiple threads. Each process always starts with just one thread which is referred to as the main thread, new threads can then be added to handle sub-tasks. All threads running in a single process shares the same resources/memory this makes communication between them easier and straightforward.
Multithreading happens when two or more threads appearing in a single process gets executed in overlapping periods of time. However, one thing that should be clear is that only a single thread can actually be executed at any given time in a process. Thus, in reality, multi-threading is not entirely simultaneous, instead, concurrency is achieved by switching very quickly between the threads, the switching is so fast that one might be forgiven for thinking that the threads are been executed in parallel.
The main purpose of multithreading is to improve program efficiency and performance as it makes it possible to execute multiple tasks simultaneously.
The threading module
In Python, multithreading is implemented using the threading
module which is available in the standard library. The module offers the necessary tools for managing and working with threads.
The Thread
class in the module, is used create, run and generally manage threads. Its syntax is as follows:
threading.Thread(target = None, name = None, args = None, kwargs = None, deamon = None)
target |
A function that will be called when the thread starts. It defaults to None , if no function is to given. |
name | The thread name. If no name is given, a unique one is auto-generated such as Thread-1 . Multiple threads can have a common name. |
daemon | A boolean value indicating whether the thread should run in the background. It defaults to False meaning that the thread does not run in the background. |
args and kwargs |
Optional argument that will be passed to the target function when the thread starts. |
Consider the following example.
import threading
import time
def square(num):
for i in range(num):
print(f"square({i}) = ", i ** 2)
time.sleep(0.1)
def cube(num):
for i in range(num):
print(f"cube({i}) = ", i ** 3, end = '\n\n')
time.sleep(0.1)
if __name__ == "__main__":
# Create two thread for each function/task
thread1 = threading.Thread(target=square, args = (6,))
thread2 = threading.Thread(target=cube, args = (6,))
#Start the threads
thread1.start()
thread2.start()
# The main thread waits for both threads to finish
thread1.join()
thread2.join()
Output:
square(0) = 0
cube(0) = 0
square(1) = 1
cube(1) = 1
square(2) = 4
cube(2) = 8
square(3) = 9
cube(3) = 27
square(4) = 16
cube(4) = 64
square(5) = 25
cube(5) = 125
Normally, we would have expected the first function i.e square
to be run until complete before the other function(cube
) is started. But as you can see in the above example, the two functions, square
and cube
, are been run concurrently
As shown from the previous example, implementing multithreading involves the following basic steps:
-
Creating the thread
This involves creating an instance of the
threading.Thread()
class. We pass the necessary arguments such as the target function, name of the thread, arguments to the target function if necessary, etc.thread1 = threading.Thread(target=square, args = (6,)) thread2 = threading.Thread(target=cube, args = (6,))
-
Starting the thread
To start the execution of the thread we call the
start()
method of thread objects .thread1.start() thread2.start()
-
Wait for the thread
When the thread starts, the main thread keeps on running as well. Calling the
join()
method of the new thread makes the main thread(or the calling thread to be precise) to pause and wait until the execution of the new thread is complete.thread1.join() thread2.join()
Daemon threads
Daemon threads are threads that runs indefinitely until the program quits. They are mostly suitable for handling regular and repeating tasks that needs to run in the background.
To create a daemon thread, we can set the daemon
parameter to True
when instantiating a Thread
object or we can call the setDaemon()
function after we have already instantiated the thread.
#import the Thread class
from threading import Thread
import time
def task():
for i in range(20):
print(i)
time.sleep(0.1)
#a background task that runs at regular intervals
def background_task(main_task):
while main_task.is_alive():
print("daemon task")
time.sleep(0.5)
if __name__ == "__main__":
#regular thread
T1 = Thread(target = task)
#daemon thread
T2 = Thread(target = background_task, kwargs = {"main_task": T1}, daemon = True)
#start the threads
T1.start()
T2.start()
T1.join()
T2.join()
Output:
daemon task
0
1
2
3
4
daemon task
5
6
7
8
9
daemon task
10
11
12
13
14
daemon task
15
16
17
18
19
daemon task
Thread objects also contains the isDaemon()
method which can be used to check whether a thread is a Daemon thread.
import threading
t1 = threading.Thread(daemon = True)
t2 = threading.Thread()
#check whether thread is a daemon
print(t1.daemon)
print(t2.daemon)
Output:
True
False
Thread states
Threads can be in any of the following states:
- New - The thread has only been instantiated but has not been started, no resources has yet been allocated for it.
- Runnable - In this state, the thread is waiting to run, it has all the necessary resources, only that the scheduler has not yet scheduled it to run.
- Live - In this state, the thread is executing its task .
- Paused - The thread has been paused in some way. For example, when it is waiting for another thread to complete the execution.
- Dead - The thread is not runnable. A thread can can reach this state naturally when its task is complete or if it has been killed unnaturally
The Thread class includes the is_live()
method which can be used to tell whether a thread is currently running.
Related articles
Posted on March 27, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.