←Back to Subject
Notes
Notes
Study material and explanations
Diagrams
Diagrams
Visualize concepts easily
Mnemonics
Mnemonics
Tricks to remember faster
One Night Prep
One Night Prep
Short and focused revision
Revision
Revision
Quick summary and key points
Important Questions
Important Questions
Practice most relevant questions
Notes

Multithreading & Multiprocessing in Python, threading Module & Thread Lifecycle

Study material and explanations for Unit 5

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

  1. 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.
  2. Multithreaded Application:
    • The Main Thread spawns multiple Child Threads (t1, t2) that run concurrently (at the same time), drastically reducing total waiting time.

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:

Life Cycle of a Thread in Python
1. New (Born)
t = Thread()
→ t.start() →
2. Runnable (Ready)
Waiting for CPU
→ Scheduler →
3. Running
Executing run()
→ Done →
5. Terminated
(Dead State)
4. Blocked / Waiting State (Triggered by sleep(), join(), or waiting for Lock.acquire())
  1. New (Born) State: A Thread object is created (t = Thread()), but start() has not been called yet.
  2. Runnable (Ready) State: Once t.start() is called, the thread enters the ready queue waiting for the OS scheduler to allocate CPU time.
  3. Running State: The CPU scheduler picks the thread and starts executing its run() method.
  4. Blocked / Waiting (Sleeping) State: The thread temporarily pauses execution because time.sleep(), t.join(), or lock.acquire() was called. Once the wait finishes, it returns to Runnable.
  5. 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 of t.start()? Why do we need the start() 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(): Calling t.start() performs special Operating System magic:
    1. It allocates a new call stack and registers a brand-new independent thread with the OS scheduler.
    2. 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