Python Notes
Complete guide to Python multithreading with threading module, Thread class, synchronization primitives, locks, semaphores, thread pools, and GIL explained with Hindi explanations.
Introduction
Multithreading is a method of concurrent execution in Python where multiple threads run simultaneously within a single process. It is most effective for I/O-bound tasks (network requests, file operations, database queries).
Important: Python's GIL (Global Interpreter Lock) prevents true parallelism in CPU-bound tasks, but threads are very effective for I/O-bound work since the GIL is released during I/O operations.
📝 Hindi Explanation
GIL is a CPython design decision that ensures thread safety but limits CPU parallelism. During I/O (network request, disk read, time.sleep), the GIL is released and other threads can run. This is why threading helps with I/O-bound tasks.
2. Thread Class Basics
[Thread-1] Starting... [Thread-2] Starting... [Thread-3] Starting... [Thread-2] Done after 1s [Thread-3] Done after 1.5s [Thread-1] Done after 2s Total time: 2.01s
⚡ 3 threads run in parallel; total time = longest thread's time (2s), not the sum (4.5s)!
[Worker-0] Processing 0 [Worker-1] Processing 10 [Worker-2] Processing 20 [Worker-3] Processing 30 [Worker-4] Processing 40 [Worker-0] Result: 0 [Worker-1] Result: 20 [Worker-2] Result: 40 [Worker-3] Result: 60 [Worker-4] Result: 80 Results: [0, 20, 40, 60, 80]
3. Daemon Threads
import threading
import time
def background_task():
"""This task will automatically end with the main thread"""
while True:
print("Background task running...")
time.sleep(1)
# Daemon thread — main program exit hone par automatically band
daemon = threading.Thread(target=background_task, daemon=True)
daemon.start()
print("Main thread working...")
time.sleep(3)
print("Main thread done — daemon will stop automatically")Main thread working... Background task running... Background task running... Background task running... Main thread done — daemon will stop automatically
💡 A daemon thread automatically terminates when the main thread exits.
# Non-daemon thread — main thread iska wait karega
def non_daemon_task():
time.sleep(5)
print("Non-daemon finished!")
t = threading.Thread(target=non_daemon_task)
t.daemon = False # Default is False
t.start()
# Main program will wait for this thread!Non-daemon finished!
⚠️ For a non-daemon thread, the main program will wait even after the main logic ends.
4. Thread Synchronization — Lock
Unsafe counter: 8743 Safe counter: 10000
⚠️ The unsafe counter gives a different value each time (race condition). With a Lock, the result is always correct.
📝 Hindi Explanation
Race Condition occurs when multiple threads access shared data without synchronization. A Lock (mutex) ensures that only one thread modifies the shared resource at a time.
5. RLock — Reentrant Lock
import threading
# Regular Lock — reentrant nahi hai (same thread deadlock)
# lock = threading.Lock()
# def recursive():
# with lock:
# with lock: # DEADLOCK! Same thread can't acquire it again
# pass
# RLock — same thread multiple times acquire kar sakta hai
rlock = threading.RLock()
def recursive_function(depth):
with rlock:
print(f"Depth: {depth}")
if depth > 0:
recursive_function(depth - 1) # Works with RLock
else:
print("Base case reached")
t = threading.Thread(target=recursive_function, args=(3,))
t.start()
t.join()
# Output:
# Depth: 3
# Depth: 2
# Depth: 1
# Depth: 0
# Base case reachedDepth: 3 Depth: 2 Depth: 1 Depth: 0 Base case reached
💡 RLock allows the same thread to acquire the lock multiple times — useful for recursive functions!
6. Semaphore — Limiting Concurrent Access
Thread-0: Waiting for resource... Thread-0: Got resource! Thread-1: Waiting for resource... Thread-1: Got resource! Thread-2: Waiting for resource... Thread-2: Got resource! Thread-3: Waiting for resource... Thread-0: Releasing resource Thread-3: Got resource! ... All threads done
💡 Semaphore allows a maximum of 3 threads to access the resource simultaneously. Remaining threads wait in the queue.
7. Event — Thread Signaling
[Waiter-0] Waiting for event... [Waiter-1] Waiting for event... [Waiter-2] Waiting for event... [TimeoutWaiter] Waiting for event... [Setter] Working... [TimeoutWaiter] Timeout! [Setter] Setting event! [Waiter-0] Event received! [Waiter-1] Event received! [Waiter-2] Event received!
💡 TimeoutWaiter timed out after 1s, while the remaining waiters kept waiting for the event and received the signal after 2s.
8. Condition Variable
import threading
import time
from collections import deque
# Producer-Consumer pattern
class BoundedBuffer:
def __init__(self, capacity):
self.capacity = capacity
self.buffer = deque()
self.condition = threading.Condition()
def produce(self, item):
with self.condition:
# Buffer full hone tak wait karo
while len(self.buffer) >= self.capacity:
print(f"Buffer full, producer waiting...")
self.condition.wait()
self.buffer.append(item)
print(f"Produced: {item} (buffer size: {len(self.buffer)})")
self.condition.notify_all() # Signal consumers
def consume(self):
with self.condition:
# Buffer empty hone tak wait karo
while not self.buffer:
print("Buffer empty, consumer waiting...")
self.condition.wait()
item = self.buffer.popleft()
print(f"Consumed: {item} (buffer size: {len(self.buffer)})")
self.condition.notify_all() # Signal producers
return item
buffer = BoundedBuffer(capacity=3)
def producer():
for i in range(7):
buffer.produce(f"item-{i}")
time.sleep(0.1)
def consumer():
for _ in range(7):
buffer.consume()
time.sleep(0.3)
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start()
t2.start()
t1.join()
t2.join()Produced: item-0 (buffer size: 1) Produced: item-1 (buffer size: 2) Produced: item-2 (buffer size: 3) Buffer full, producer waiting... Consumed: item-0 (buffer size: 2) Produced: item-3 (buffer size: 3) Consumed: item-1 (buffer size: 2) Produced: item-4 (buffer size: 3) Consumed: item-2 (buffer size: 2) Produced: item-5 (buffer size: 3) Consumed: item-3 (buffer size: 2) Produced: item-6 (buffer size: 3) Consumed: item-4 (buffer size: 2) Consumed: item-5 (buffer size: 1) Consumed: item-6 (buffer size: 0)
📝 Hindi Explanation
Condition Variable lets a thread wait for a specific condition to become true.wait()releases the lock and blocks.notify()/notify_all()wakes up waiting threads when the condition is met.
9. Thread-Safe Queue
Produced: task-0 Produced: task-1 Worker-0: processed task-0 Worker-1: processed task-1 ... Total results: 10
10. ThreadPoolExecutor
Task 3: {'id': 3, 'data': 'response_3'}
Task 1: {'id': 1, 'data': 'response_1'}
Task 0: {'id': 0, 'data': 'response_0'}
Task 4: {'id': 4, 'data': 'response_4'}
Task 2: {'id': 2, 'data': 'response_2'}
...
Results: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]💡as_completed()returns results in completion order, so the order appears random.map()maintains the original order.
📝 Hindi Explanation
ThreadPoolExecutor simplifies thread management. It reuses threads to avoid create/destroy overhead.submit()returns aFutureobject for result tracking, andmap()applies a function to all inputs in parallel.
11. Thread Local Storage
[Thread: Worker-0] Processing for user: user_0 [Thread: Worker-1] Processing for user: user_1 [Thread: Worker-2] Processing for user: user_2 [Thread: Worker-3] Processing for user: user_3 [Thread: Worker-4] Processing for user: user_4
💡 Each thread has its own thread_local.user — no interference!12. Best Practices
Summary
| Synchronization | Use Case |
|---|---|
Lock | Basic mutual exclusion |
RLock | Reentrant lock (same thread) |
Semaphore | Limit concurrent access |
Event | Thread signaling |
Condition | Complex wait conditions |
Queue | Thread-safe communication |
Thread.local() | Per-thread storage |
Remember: Use threading for I/O-bound tasks (network, disk, DB). For CPU-bound tasks, use multiprocessing module to bypass the GIL.⚠️ Common Mistakes
1. Using threads for CPU-bound tasks while ignoring the GIL ❌
Fix: Use the multiprocessing module for CPU-bound tasks.2. Forgetting to use Lock (Race Condition) ❌
# ❌ Wrong: Shared mutable state without lock
balance = 1000
def withdraw(amount):
global balance
if balance >= amount:
balance -= amount # Race condition!Fix: Always use threading.Lock() for shared mutable data.3. Deadlock create karna ❌
# ❌ Wrong: Two locks acquired in different order
lock_a = threading.Lock()
lock_b = threading.Lock()
# Thread 1: lock_a → lock_b
# Thread 2: lock_b → lock_a ← DEADLOCK!Fix: Always acquire locks in the same order, or use the timeout parameter.4. Forgetting to call join() ❌
# ❌ Wrong: Using result before thread completes
t = threading.Thread(target=some_task)
t.start()
# used result without join() — thread hasn't completed yet!
print(result)Fix: Call t.join() before accessing the result — this ensures the thread has completed.5. Too many threads create karna ❌
Fix: Use ThreadPoolExecutor(max_workers=N). Reusing threads reduces overhead.6. Doing important work in daemon threads ❌
# ❌ Wrong: Critical task in daemon thread
t = threading.Thread(target=save_to_database, daemon=True)
t.start()
# Main exit → data LOST! Daemon threads forcefully kill ho jaate hainFix: Use non-daemon threads for critical tasks and properly join() them.7. Thread mein exception silently swallow hona ❌
# ❌ Wrong: Exception raised in thread — main thread doesn't know
def risky_task():
raise ValueError("Something went wrong!")
t = threading.Thread(target=risky_task)
t.start()
t.join()
# No error visible in main thread!Fix: UseThreadPoolExecutor—future.result()will propagate exceptions. Or use manual try/except with an error queue.
✅ Key Takeaways
- 🧵 Multithreading is best for I/O-bound tasks — network calls, file I/O, database queries all get real speedup
- 🔒 GIL blocks parallelism in CPU-bound tasks — use the
multiprocessingmodule for CPU-heavy work - 🛡️ Always protect shared mutable state with a Lock — without a lock, race conditions and unpredictable bugs occur
- 🏊 ThreadPoolExecutor is better than manual thread management — thread reuse, exception handling, and cleanup are handled automatically
- 📦
queue.Queueis the safest option for thread communication — built-in thread safety, no manual locking needed - 👻 Daemon threads are for background tasks — never do critical work in daemon threads
- 🔄 RLock for recursive locking, Semaphore for concurrent access limits — choose the right synchronization primitive
- ⏱️ Always use
join()— essential to ensure thread completion - 🎯
as_completed()gets results faster — you can process the fastest task's result first - 📍 Use thread-local storage for per-thread data isolation — a common pattern for storing request context in web frameworks
❓ FAQ
Q1: What is the difference between a Thread and a Process?
A: A Thread is a lightweight execution unit within a process. Threads share the same memory space (fast communication), while processes have separate memory (need IPC). Threads are cheaper to create but share GIL; processes have true parallelism.
Q2: How to bypass GIL?
A: Use the multiprocessing module for CPU-bound tasks, which runs separate processes (each with its own GIL). concurrent.futures.ProcessPoolExecutor provides a familiar interface identical to ThreadPoolExecutor.
Q3: How many threads should you create?
A: For I/O-bound tasks: typically min(32, os.cpu_count() + 4) is a good starting point (Python 3.8+ default). For CPU-bound work, threads provide no benefit — use multiprocessing instead.
Q4: threading.Thread vs ThreadPoolExecutor — when to use which?
A: Prefer ThreadPoolExecutor — it manages thread lifecycle, properly propagates exceptions, and reuses threads. Use direct threading.Thread only when you need fine-grained control (custom daemon behavior, specific thread naming).
Q5: How do you detect race conditions?
A: Tools: the threading module's built-in -X faulthandler, py-spy for profiling, and ThreadSanitizer (for C extensions). In code review, look for: any shared mutable state being accessed without a lock.
Q6: Does threading.Lock() slow down performance?
A: Yes, there's some overhead (lock acquire/release), but it's necessary for correctness. To improve performance: minimize lock scope (hold it for the shortest time), use RLock only when needed, and consider lock-free alternatives like queue.Queue.
Q7: What is the difference between thread-safe and non-thread-safe?
A: Thread-safe code produces correct results even when called simultaneously from multiple threads. In Python, list.append() is technically atomic (due to GIL), but compound operations like check-then-act are not. Always use locks for compound operations.
Q8: What to do if a thread hangs in a thread pool?
A: Use future.result(timeout=N) — it raises TimeoutError after the timeout. In ThreadPoolExecutor, individual threads can't be cancelled (Python limitation). Design around it: set operation-level timeouts, use non-blocking I/O, or restructure to make tasks cancellable.
🎯 Interview Questions
Q1: What is the GIL in Python and how does it affect multithreading?
A: GIL (Global Interpreter Lock) is a mutex in the CPython interpreter that ensures only one thread executes Python bytecode at a time. This means CPU-bound multithreading gets no speedup. However, during I/O operations the GIL is released, allowing other threads to run — making threading effective for I/O-bound work.
Q2: What is the difference between threading.Lock() and threading.RLock()?
A: Lock (mutex) is a binary lock — if the same thread tries to acquire it again, deadlock occurs. RLock (Reentrant Lock) allows the same thread to acquire it multiple times (must release the same number of times). Use RLock for recursive functions or nested calls.
Q3: What is a race condition? Explain with an example.
A: A race condition occurs when multiple threads simultaneously access/modify a shared resource and the result depends on thread scheduling. Classic example: two threads reading a bank balance, each deciding to withdraw, and both succeeding even though funds are insufficient.
# Example: balance = 100, two threads withdraw 80
# Thread A: reads balance=100, checks 100>=80 ✓
# Thread B: reads balance=100, checks 100>=80 ✓ (before A writes)
# Thread A: balance = 100-80 = 20
# Thread B: balance = 100-80 = 20 (should be -60 or rejected!)Solution: Use a Lock to make read-check-modify atomic.
Q4: What is a deadlock and how do you prevent it?
A: Deadlock occurs when two or more threads are waiting for each other's locks indefinitely. Conditions (Coffman): Mutual exclusion, Hold and wait, No preemption, Circular wait. Prevention: Lock ordering (always acquire in same order), timeouts, try_lock, or lock hierarchies.
Q5: What are the advantages of ThreadPoolExecutor over manual thread management?
A: (1) Thread reuse — avoids create/destroy overhead, (2) Automatic cleanup — context manager properly shuts down threads, (3) Exception propagation — future.result() raises the exception from the thread, (4) Backpressure — max_workers limits concurrent threads.
Q6: Which data structures in Python are thread-safe?
A: (1) queue.Queue, queue.LifoQueue, queue.PriorityQueue — fully thread-safe with blocking, (2) collections.deque — append() and popleft() are atomic (good for simple queues), (3) logging module — thread-safe by design. Most other built-in types need explicit locking for compound operations.
Q7: Daemon thread vs Non-daemon thread — when to use which?
A: Daemon threads: Background tasks that should automatically terminate when the program exits — logging, monitoring, heartbeat, garbage collection. t.daemon = True before t.start(). Non-daemon threads: Critical tasks that must complete — file writes, database transactions. Program waits for them.
Q8: What is the difference between concurrent.futures.as_completed() and executor.map()?
A: map(): Returns results in original order, simpler API, exceptions are raised when you iterate to that result. as_completed(): Returns results in completion order (fastest first), more control, immediate exception access via future.result(). Use as_completed() when you want to process results as soon as they're available.
Q9: How do you implement the Producer-Consumer pattern in Python?
A: Best approach: Use queue.Queue — it handles locking internally.
import queue, threading
q = queue.Queue(maxsize=10)
def producer():
for i in range(20):
q.put(f"item-{i}") # Blocks if full
q.put(None) # Sentinel
def consumer():
while True:
item = q.get() # Blocks if empty
if item is None: break
process(item)
q.task_done()Key points: maxsize provides backpressure, task_done() + join() enable completion tracking, sentinel values enable graceful shutdown.
Q10: How do you handle exceptions in multithreading?
A: Exceptions raised in a thread don't automatically propagate to the main thread. Solutions: (1) ThreadPoolExecutor + future.result() — best approach, exceptions propagate when you access the result. (2) Manual try/except within the thread with an error queue. (3) Custom Thread subclass that stores exceptions.
# Best practice with ThreadPoolExecutor
with ThreadPoolExecutor() as executor:
future = executor.submit(risky_function)
try:
result = future.result(timeout=10)
except Exception as e:
print(f"Thread failed: {e}")Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Python Multithreading.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Python Master Course topic.
Search Terms
python-master-course, python master course, python, master, course, advanced, multithreading, python multithreading
Related Python Master Course Topics