Difference between process, thread, coroutine
Process
A process is an independent unit of execution with its own memory space. Communication between processes requires inter-process communication (IPC) mechanisms.
Example: Running python app.py
twice creates two separate processes.
from multiprocessing import Process
def worker(num):
print(f"Process {num} running")
if __name__ == "__main__":
p1 = Process(target=worker, args=(1,))
p2 = Process(target=worker, args=(2,))
p1.start()
p2.start()
p1.join()
p2.join()
# Output:
# Process 2 running
# Process 1 running
Thread
A thread is a lightweight unit of execution inside a process. Multiple threads within the same process share the same memory space.
Example: Web browsers use multiple threads (one for UI, one for network, etc.).
from threading import Thread
def task(name):
print(f"Thread {name} is running")
t1 = Thread(target=task, args=("A",))
t2 = Thread(target=task, args=("B",))
t1.start()
t2.start()
t1.join()
t2.join()
# Output:
# Thread A is running
# Thread B is running
Coroutine
A coroutine is a special function that can pause and resume execution. They are cooperative, i.e. they yield control instead of being preempted like threads. They are used in async programming.
Note: Coroutines run on a single thread but handle many tasks by pausing (
await
) instead of blocking.
Example:
import asyncio
async def task(name):
print(f"Coroutine {name} started")
await asyncio.sleep(1)
print(f"Coroutine {name} finished")
async def main():
await asyncio.gather(task("X"), task("Y"))
asyncio.run(main())
Creating, starting, and stopping threads with the threading module
Creating and starting threads
Threads in Python are created using the Thread
class from the threading
module.
The typical lifecycle of a thread looks like this:
- Create an object of the
Thread
class. - Start the thread using
.start()
. - The thread runs the target function or its
run()
method.
Note: The
.join()
method can be used to wait for the thread to finish. It is optional but recommended.
Example:
from threading import Thread
import time
def worker():
print("Thread started")
time.sleep(2) # simulate some work
print("Thread finished")
# Step 1: Create a thread with a worker function as a target
t = Thread(target=worker)
# Step 2: Start the thread
t.start()
# Step 3 (optional but good practice): Wait for the thread to finish
t.join()
print("Main program finished")
Output:
Thread started
Thread finished
Main program finished
Stopping threads
Python does not provide a direct method to stop threads. This is intentional because abruptly killing a thread can leave resources in an inconsistent state (for example, file handles, database connections).
Instead, the common pattern is to use a shared flag that the thread checks regularly to know when to exit.
Example:
from threading import Thread
import time
# Shared flag to tell the thread when to stop
stop_thread = False
def worker():
while not stop_thread:
print("Working...")
time.sleep(1)
print("Thread stopping gracefully")
# Start the thread
t = Thread(target=worker)
t.start()
# Let it run for a few seconds
time.sleep(3)
# Change flag -> signals thread to stop
stop_thread = True
# Wait for the thread to finish
t.join()
print("Main program finished")
Output:
Working...
Working...
Working...
Thread stopping gracefully
Main program finished
Note: For more advanced use cases,
threading.Event
is often used instead of a simple boolean flag, because it is thread-safe and avoids race conditions.
Various ways of creating a thread
1. Using the Thread class with a target function
This is the simplest and most common method, passing a function to the Thread
constructor.
from threading import Thread
def task():
print("Task executed")
# Create and start the thread
t = Thread(target=task)
t.start()
t.join()
# Output: Task executed
2. Extending the Thread class
In case there is a need to encapsulate thread logic in a class (for example, pass state, override behaviour, make code more reusable), subclass threading.Thread
can be used. In this case, the run()
method needs to be overridden.
from threading import Thread
class MyThread(Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f"Task executed inside custom Thread class by {self.name}")
# Create and start the custom thread
t = MyThread("Worker-1")
t.start()
t.join()
# Output: Task executed inside custom Thread class by Worker-1
Why super().__init__()
is called?
The Thread
class (from threading
) already has its own __init__
method, which sets up internal bookkeeping:
- assigning a unique identifier to the thread
- initialising the thread state (new, started, finished, etc.)
- preparing resources needed by the operating system to handle the thread
So, if super().__init__()
is not called, all that setup will not happen, and the custom thread class may not behave like a proper thread. It will raise an error like RuntimeError: thread.__init__() not called
Advantage of creating your own thread class
- Thread behavior can be customized by overriding
run()
or adding extra attributes/methods. - This makes code cleaner when there is a need to manage state or handle complex tasks.
Thread identifier vs Native Identifier
Thread identifier (ident
):
The thread identifier is an integer assigned by Python's threading
module that is unique to the thread for its entire lifetime. You can get this ID using the threading.get_ident()
function or by accessing the .ident
attribute of a Thread
object.
Example:
import threading
def worker():
print(f"[Worker] Running in Thread Name: {threading.current_thread().name}, ID: {threading.get_ident()}")
# Main thread info
print(f"[Main] Thread Name: {threading.current_thread().name}, ID: {threading.get_ident()}")
t = threading.Thread(target=worker)
t.start()
t.join()
print(f"[Main] After worker finished, Worker thread ID was: {t.ident}")
Output:
[Main] Thread Name: MainThread, ID: 8456970432
[Worker] Running in Thread Name: Thread-1 (worker), ID: 6141423616
[Main] After worker finished, Worker thread ID was: 6141423616
Note: You can set the name of a thread using
Thread.name
orthreading.current_thread().name
Native identifier (native_id
):
It is the identifier assigned by the underlying OS (available from Python 3.8+). It is useful when debugging at the OS-level or with external tools.
import threading
def worker():
print("Native ID:", threading.get_native_id())
t = threading.Thread(target=worker)
t.start()
t.join()
# Output: Native ID: 2386631
Further topics related to Threading and Multithreading, along with practice sets and interview questions, will be covered in the upcoming article. Stay Tuned!
Top comments (0)