Multithreading & Multiprocessing in Python (threading Module)
Modern applications often need to perform multiple tasks simultaneously — for example, downloading a file in the background while keeping the user interface responsive. Python achieves concurrent execution using Multithreading and Multiprocessing.
1. Multithreading vs Multiprocessing in Python
- Thread: The smallest unit of execution within a process (also called a Lightweight Process). Multiple threads inside the same process share the same memory space.
- Process: An independent program in execution (a Heavyweight Process) with its own separate memory space.
| Feature | Multithreading (threading module) |
Multiprocessing (multiprocessing module) |
|---|---|---|
| Definition | Running multiple threads concurrently within a single process | Running multiple independent processes across multiple CPU cores |
| Memory Space | All threads share the same memory and global variables | Each process has its own separate memory space |
| Creation Speed | Fast and lightweight (low overhead) | Slower and heavyweight (high memory overhead) |
| Crash Impact | If one thread crashes, it can affect the entire process | Processes are isolated; one crashing doesn’t kill others |
| Best Used For | I/O-Bound Tasks (File downloading, network requests, DB queries) | CPU-Bound Tasks (Heavy math, video encoding, Machine Learning) |
2. Single-Threaded vs Multithreaded Application
- Single-Threaded Application:
- Every Python program starts with one default thread called the Main Thread.
- Tasks execute strictly one after another (sequentially). If Task 1 takes 10 seconds, Task 2 must wait 10 seconds before starting.
- Multithreaded Application:
- The Main Thread spawns multiple Child Threads (
t1,t2) that run concurrently (at the same time), drastically reducing total waiting time.
- The Main Thread spawns multiple Child Threads (
3. Creating Threads in Python (threading Module)
Python provides the built-in threading module to create and manage threads. There are 3 standard ways to create a thread in Python:
Method 1: Creating a Thread Using a Function (target=function)
import threading
def print_numbers():
for i in range(1, 4):
print(f"Number: {i}")
# Create Thread object and pass function name to 'target'
t1 = threading.Thread(target=print_numbers)
t1.start() # Starts the new thread
Method 2: By Inheriting the Thread Class (Most Common in Exams!)
You create a custom class that inherits from threading.Thread and override its run() method:
from threading import Thread
class MyThread(Thread):
# Override the run() method with the task logic
def run(self):
for i in range(1, 4):
print(f"Child Thread executing step {i}")
# Create object and start thread
t = MyThread()
t.start() # Internally invokes run() in a separate thread!
Method 3: Using a Callable Object (Class without Inheriting Thread)
You can create a normal class (or a class defining __call__()) and pass its method/instance to Thread(target=...):
from threading import Thread
class TaskWork:
def execute_task(self, name):
for i in range(1, 4):
print(f"{name} running iteration {i}")
obj = TaskWork()
# Pass the object's bound method as a callable target
t1 = Thread(target=obj.execute_task, args=("Worker-1",))
t1.start()
4. Life Cycle of a Thread in Python
During its lifetime, a thread transitions through 5 distinct states:
t = Thread()t.start() →Waiting for CPU
Executing
run()(Dead State)
sleep(), join(), or waiting for Lock.acquire())
- New (Born) State: A
Threadobject is created (t = Thread()), butstart()has not been called yet. - Runnable (Ready) State: Once
t.start()is called, the thread enters the ready queue waiting for the OS scheduler to allocate CPU time. - Running State: The CPU scheduler picks the thread and starts executing its
run()method. - Blocked / Waiting (Sleeping) State: The thread temporarily pauses execution because
time.sleep(),t.join(), orlock.acquire()was called. Once the wait finishes, it returns to Runnable. - Terminated (Dead) State: Once the
run()method finishes executing, the thread dies and cannot be restarted.
5. Can We Call run() Directly? Need for start() Method
This is a very important DAVV BCA exam question:
Question: Can we call
t.run()directly instead oft.start()? Why do we need thestart()method?
Answer:
- Yes, syntactically you can call
t.run()directly without getting an error, BUT it will NOT create a new separate thread! - If you call
t.run()directly, it behaves just like a normal function call executed by the Main Thread (Single-threaded execution). No multithreading happens! - Why we need
t.start(): Callingt.start()performs special Operating System magic:- It allocates a new call stack and registers a brand-new independent thread with the OS scheduler.
- It then automatically invokes the
run()method inside that new thread concurrently!
| Feature | Calling t.start() |
Calling t.run() Directly |
|---|---|---|
| New Thread Created? | Yes, creates a new independent thread | No, runs inside the existing Main Thread |
| Execution Mode | Concurrent (Multithreaded) | Sequential (Single-threaded) |
| How Many Times Callable? | Can be called only ONCE per thread object | Can be called multiple times like a normal method |