An Introduction to Multithreading
Hello and welcome to an Introduction to Multithreading.
This is a beginner-friendly intro to multithreading, using simple analogies, step-by-step topics, and practical examples.
What is Concurrency?
Concurrency means doing multiple things at once — or at least handling multiple tasks in a way that makes them feel like they’re happening at the same time. It’s about structure:
Concurrency is how a software program manages many tasks that overlap in time.
Multithreading is one way to implement concurrency. It lets a program create multiple threads — like mini workers — that can run in parallel or take turns running. This way, the program can juggle several tasks at once without waiting for one to finish before starting another.
What is a Thread?
A thread is a unit of execution within a process.
A process is like a running program — it has memory, code, and data.
When we run a Python script, a Node.js app, or a compiled Java program, the operating system starts a new process for that program.
That process is given its own isolated environment, which includes:
Memory space The process has its own dedicated memory — separate from other processes. Includes:
- Stack: For function calls and local variables
- Heap: For dynamically allocated data (e.g., objects, arrays)
- Global/Static Memory: For global variables
- Code (Text) Segment: Where the program’s compiled instructions live
Example:
name = "Alice"
count = 3
- These variables exist in the memory of this process only.
- Another process running the same script will have its own copy.
Code Execution (Instruction Pointer) The process executes code — one instruction at a time (or many via threads).
- The CPU executes the program instructions stored in memory.
- The instruction pointer keeps track of the current line of code being executed.
Example:
print("Hello")
x = 5 + 2
- The instruction pointer first runs print(), then moves to x = 5 + 2, and so on.
Resources (e.g., file handles, sockets)
A process can access OS-level resources — but they are private to the process unless explicitly shared.
Examples:
-
File Handles:
- open("file.txt") gives us process a handle to read/write a file.
- Other processes don’t automatically see this file handle.
-
Network Sockets:
- A process listening on port 3000 (e.g., a web server) uses a socket bound to that port.
-
Environment Variables:
-Things like DATABASE_URL that influence process behavior.
Example:
f = open('data.txt')
print(f.read())- The f file handle lives in this process only.
A thread is a smaller, lighter-weight component inside that process.
- Threads share the same memory space of the process.
- Each thread performs a specific task, often running concurrently with others.
Process Isolation
- Each process is isolated by the operating system.
- It cannot access another process’s memory or resources unless explicitly allowed (via inter-process communication, shared memory, etc.)
Example of a process: (Python)
import os
name = "Alice" # Variable (memory)
with open('file.txt') as f: # File handle (resource)
print(f.read()) # Execution (code)
print(os.getenv("PATH")) # Environment variable (resource)
Processes are the foundation of how programs run. Threads are units of execution within them — and understanding processes helps us grasp how multithreading and multiprocessing operate in modern software.
Why Use Threads?
Threads are useful when our program needs to handle multiple tasks at once, especially when some tasks involve waiting (e.g., for a file or network response).
Common Reasons to Use Threads:
- Concurrency: Perform multiple tasks at the same time within a single program.
- Responsiveness: Keep programs responsive while waiting for slow tasks.
- Efficiency: Use system resources better for I/O-bound operations.
Multithreading
To understand multithreading, let's use a relatable analogy — a restaurant kitchen preparing a customer's order.
A customer places an order for a complete meal that requires three preparation steps:
- Chop vegetables
- Boil pasta
- Prepare sauce
These tasks can ideally be done in parallel, but that depends on how many chefs (workers/threads) are available in the kitchen (process).
Single-Threaded Kitchen (One Chef)
In a single-threaded environment, there's only one chef, who must complete each step one after another.
Characteristics:
- Simple to manage
- No coordination needed
- Slower: the customer waits for all steps to finish sequentially
Timeline:
Customer Order Received → Chop Veggies → Boil Pasta → Prepare Sauce → Serve
Multi-Threaded Kitchen (Multiple Chefs)
Now imagine we have three chefs, each handling one step of the process — simultaneously.
Characteristics:
- Requires coordination (shared resources, timing)
- Greatly reduces total prep time
- Efficient for tasks that can run in parallel
Timeline (Concurrent Execution):
Customer Order Received
→ [Chop Veggies] ← Thread 1
→ [Boil Pasta] ← Thread 2
→ [Prepare Sauce] ← Thread 3
→ Wait for all → Serve
Diagram (Multi-Threaded / Multi-Chef)
Takeaways
Feature | Single-Threaded | Multi-Threaded |
---|---|---|
Workers | 1 Chef | Multiple Chefs |
Execution | Sequential | Concurrent/Parallel |
Time Efficiency | Slower | Faster |
Complexity | Low | Moderate (needs coordination) |
Real-World Mapping
Kitchen Concept | Programming Concept |
---|---|
Kitchen | Hardware |
Recipe | List of Tasks |
Customer | User |
Server | Controller |
Ingredients | Memory / Data |
Chef | Thread |
Multithreading helps software do more in less time by letting independent tasks run simultaneously — just like a well-staffed kitchen!
Thread Lifecycle in Python
In Python, threads are created and managed using the threading
module.
The basic lifecycle of a Python thread includes:
- New (Created)
- Runnable (Ready to Run)
- Running
- Blocked / Waiting
- Terminated (Dead)
1. New (Created)
A thread is created using threading.Thread
, but it doesn’t start executing until .start()
is called.
import threading
def task():
print("Thread is running")
t = threading.Thread(target=task) # Thread is in New state
What Does It Mean to Create a New Thread?
A new thread means: we have defined a unit of work that can be executed independently, but it hasn’t started running yet.
When we create a new thread in Python using:
t = threading.Thread(target=task)
We now have a Python object that represents a thread. Here's what happens under the hood.
-
Python creates an instance of the Thread class.
-
This instance knows:
- What function to run (target=task)
- Any arguments for that function
- Its identity (name, ID)
we can check the thread’s state before starting:
import threading
def task():
print("Running...")
t = threading.Thread(target=task)
print(t.is_alive()) # False — thread hasn't started yet
t.start()
print(t.is_alive()) # True — thread is now running
2. Runnable (Ready to Run)
Once we call .start() on a thread, it enters the Runnable state — it’s ready to be scheduled by the Python interpreter.
What Does "Runnable" Mean?
- The thread has been handed off to the Python interpreter.
- It is now eligible to start running, but may not start immediately.
- The interpreter (and OS) decides when it actually runs, depending on system load and thread scheduling.
In short:
Runnable = "I’m ready! Put me in, coach!" (But the coach decides when we play.)
What Happens When .start()
is Called?
Calling .start()
transitions a thread from New to Runnable. Here’s what happens step-by-step:
-
Python asks the operating system to create a new thread.
Python initializes a native thread using the underlying OS.🛠️ Imagine Python saying: “Hey OS, I need a new helper to run this task.”
-
The thread is added to Python’s list of waiting threads.
It’s now registered with Python’s internal thread scheduler.📝 It’s like putting our name on the waitlist for the CPU.
-
The thread becomes eligible to run — but might have to wait.
Just because the thread is ready doesn't mean it runs immediately.⏳ “I’m ready!” it says, but it might wait if the system is busy.
-
The thread starts running when scheduled.
As soon as the system has room, Python lets the thread begin its task.🏃 When it’s our thread’s turn, it jumps in and gets to work.
Example
import threading
import time
def task():
print("Thread is running")
time.sleep(2)
print("Thread finished")
t = threading.Thread(target=task)
print("Before start: is_alive =", t.is_alive()) # False
t.start() # Now in Runnable state
print("After start: is_alive =", t.is_alive()) # True (Runnable or Running)
Key Characteristics of Runnable State
- The thread exists in the system.
- The thread has not necessarily started running yet.
- It's waiting to be scheduled by Python and the OS.
3. Running
The thread enters the Running state when the operating system decides it's time to start executing our task — it's no longer just ready, it's now actually running.
Even after we call .start()
, the thread does not immediately begin execution.
It enters the Runnable state first, and then waits for the operating system to decide when to run it.
🕰️ Think of it like being called in from a waiting room — our thread is ready, but still has to wait its turn.
Who decides when a thread runs?
That’s the job of the Operating System’s thread scheduler. The scheduler uses algorithms to decide:
- Which threads to run
- For how long
- In what order
Common Scheduling Strategies
Algorithm | Description |
---|---|
Round-Robin | Threads take turns in equal time slices |
Priority Scheduling | Higher-priority threads run first |
Multilevel Queue | Threads are grouped by type and priority |
Shortest Job First | Shortest estimated task runs first (rare for threads) |
🔄 On modern systems, a combination of these algorithms is often used.
Once Running...
The thread:
- Gets CPU time to run the
target
function (e.g.,task()
) - Continues running until:
- It finishes
- It blocks (e.g., sleep or waiting for I/O)
- It is preempted by another thread
import threading
import time
def task():
print("Thread is now running") # Running state
time.sleep(2)
print("Thread has finished")
t = threading.Thread(target=task)
t.start() # Runnable
# OS decides when thread actually becomes Running
Key Takeaways
.start()
does not mean the thread runs immediately.- The OS schedules the thread based on system rules and load.
- The thread moves to Running only when the CPU is ready for it.
4. Switching Between Running and Runnable
In Python (and most operating systems), a thread can switch back and forth between the Running and Runnable states multiple times during its lifetime.
Why Does This Happen?
Even after a thread starts running, it might be paused (preempted) so another thread can run. When this happens:
- The running thread is moved back to the Runnable state
- It’s still ready to run, just waiting its turn again
When Does a Running Thread Become Runnable Again?
Here are some common scenarios:
- Time Slice Expired (Preemption)
Operating systems use time slicing to give all threads a fair chance.
🕒 After using its time slice, a thread is paused and moved back to the Runnable queue.
import threading
import time
def task():
for i in range(5):
print(f"Task running: {i}")
time.sleep(0.1) # Simulate work
threads = [threading.Thread(target=task) for _ in range(3)]
for t in threads:
t.start()
- Threads take turns running
- When one’s time slice ends, it’s paused and becomes Runnable again
- Higher-Priority Thread is Scheduled
If another thread with higher priority becomes runnable, the OS may pause the current thread.
The current thread yields the CPU and is put back into the Runnable state.
- Thread Calls
yield
(Voluntary Pause)
Although Python doesn't have a built-in Thread.yield()
like Java, we can simulate cooperative multitasking:
time.sleep(0) # Yield control to another thread
This tells Python: “I'm okay with pausing now if someone else is ready.”
Summary
- A thread starts in the Runnable state after .start().
- It moves to Running when the OS schedules it.
- It can return to Runnable if preempted, or voluntarily yields control.
- Eventually, when the thread finishes, it moves to Terminated.
Reason for Switch | From | To | Why It Happens |
---|---|---|---|
Time slice expired | Running | Runnable | OS wants to give other threads a turn |
Higher-priority thread | Running | Runnable | Another thread needs CPU more urgently |
Voluntary yield | Running | Runnable | Thread chooses to let others run |
A thread doesn't run non-stop — it shares CPU time with others. Understanding this helps we design better concurrent programs that play well with others.
5. Blocked / Waiting
A thread enters the Blocked or Waiting state when it temporarily cannot continue execution — usually because it’s waiting for some external condition or resource.
import time
def task():
time.sleep(2) # Thread is sleeping (Blocked)
t = threading.Thread(target=task)
t.start()
What Does It Mean for a Thread to Be Blocked or Waiting?
Even if a thread is actively running, it might pause itself or get paused by the system when it needs to:
- Wait for I/O (e.g., file read/write, network call)
- Sleep for a certain duration (
time.sleep
) - Wait for a lock (
threading.Lock
) - Wait for another thread to finish (
t.join()
)
Examples of Blocked/Waiting Situations
- Sleeping (Explicit Pause)
import time
def task():
print("Sleeping...")
time.sleep(3) # Thread is voluntarily paused
print("Woke up!")
t = threading.Thread(target=task)
t.start()
Here, the thread enters the Blocked state while sleeping and automatically resumes after 3 seconds.
- Waiting for a Lock
import threading
import time
lock = threading.Lock()
def task():
with lock:
print("Thread has the lock")
time.sleep(2)
print("Thread releasing lock")
t1 = threading.Thread(target=task)
t2 = threading.Thread(target=task)
t1.start()
t2.start()
t1
acquires the lock and sleepst2
becomes Blocked, waiting for the lock to be released
- Waiting for I/O
def slow_read():
with open("large_file.txt", "r") as f:
content = f.read() # Blocks while reading
t = threading.Thread(target=slow_read)
t.start()
- Thread enters Blocked state while reading from disk
When a thread performs file I/O like f.read()
, it may enter the Blocked state — but that doesn't mean it’s frozen or stuck. Instead, the operating system takes over.
Here’s the explanation of what really happens under the hood:
Understanding Blocking Thread with File I/O
-
Python Calls into the OS
When we writef.read()
, Python uses a system call (likeread()
in C or Unix) to tell the OS:“Please read this file from disk into memory.”
-
The OS Handles the I/O
Disk access is slow compared to RAM or CPU. The OS takes responsibility for handling the read operation — it talks to the disk while wer thread waits. -
Thread Is Blocked
While the OS waits for the disk, it puts wer thread to sleep. The thread is now Blocked:- It uses no CPU time
- It cannot continue until the data is available
-
Thread Is Woken Up
Once the OS receives the data from disk, it wakes up the thread and lets it resume where it left off.
Visualizing File/IO
Threads in the Blocked state are not dead — they’re just temporarily waiting, and will resume when the resource or condition becomes available.
6. Terminated (Dead)
A thread enters the Terminated state after it finishes executing the task we gave it. This is the final stage of the thread’s lifecycle.
import threading
import time
def task():
print("Task started")
time.sleep(1)
print("Task complete")
t = threading.Thread(target=task)
t.start()
t.join() # Waits for the thread to finish
print("Main thread: Thread has terminated.")
What Does It Mean for a Thread to Be Terminated?
- The thread has completed its work (either returned or exited).
- It can no longer be restarted or reused.
- Its memory and system resources are cleaned up by Python and the OS.
Key Characteristics of the Terminated State
- The thread has exited the
target()
function. - It may have completed normally or raised an exception — either way, it’s done.
- It still exists as an object in Python, but it is no longer active.
print(t.is_alive()) # False
🚫 Common Misconception
You cannot restart a terminated thread.
Trying to call .start()
again on a completed thread will raise a RuntimeError
.
t.start() # ❌ This will raise an error if already started and finished
Summary
Multithreading allows programs to handle multiple tasks simultaneously by using threads, which are lightweight units of execution inside a process. Threads share memory but can independently execute tasks, making programs more responsive and efficient — especially for I/O-bound operations.
Concept | Explanation |
---|---|
Concurrency | Managing overlapping tasks for better responsiveness |
Thread | A unit of work within a process that can run independently |
Process | An isolated running program; contains one or more threads |
Multithreading | Running multiple threads concurrently within a single process |
Runnable | Thread is ready but waiting to be scheduled by OS |
Running | Thread is actively executing |
Blocked/Waiting | Thread is paused due to I/O, sleep, or resource locks |
Terminated | Thread has completed execution and can't be restarted |
Python Thread Lifecycle
Enjoyed this guide?
If this helped you understand multithreading a little better, consider sharing it with someone who might benefit too!
If you have feedback, or if you like my teaching style and want to learn about another topic please contact me here — I’d love to hear from you!