Python Notes
Master Python multiprocessing with Process class, Pool, shared memory, inter-process communication, pipes, queues, and comparison with multithreading.
Introduction
Multiprocessing provides true parallel execution in Python. Each process uses its own memory space and runs independently of the GIL (Global Interpreter Lock). This makes it ideal for CPU-bound tasks.
Multithreading vs Multiprocessing:
- Threads: Shared memory, GIL limited, I/O-bound
- Processes: Separate memory, true parallelism, CPU-bound
2. Process Class — Full Control
Process 0 (PID: 23401) starting... Process 1 (PID: 23402) starting... Process 2 (PID: 23403) starting... Process 3 (PID: 23404) starting... Process 0 done: 333328333350000 Process 1 done: 2666626666700000 Process 2 done: 8999925000050000 Process 3 done: 21333173333400000 Results: Task 0: 333,328,333,350,000 Task 1: 2,666,626,666,700,000 Task 2: 8,999,925,000,050,000 Task 3: 21,333,173,333,400,000
3. Process Pool — map() and starmap()
CPUs available: 8 Squares: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] Powers of 2: [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024] Async squares: [0, 1, 4, 9, 16] 1 4 9 16 25 36 49 64 81 100
📝 Hindi Explanation
Pool automatically creates and manages processes. map() takes a list of inputs, applies the function to each, and returns a results list in order. This is the simplest way to parallelize batch work.4. Shared Memory — Value and Array
Final counter: 5000 Shared array: [0.0, 0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0]
5. Inter-Process Communication — Pipe
Sending: Hello Sending: World Sending: Python Sending: Multiprocessing Received: Hello Received: World Received: Python Received: Multiprocessing All messages transferred! Request: hello -> Response: Processed: HELLO Request: world -> Response: Processed: WORLD Request: python -> Response: Processed: PYTHON
📝 Hindi Explanation
Pipe is a direct communication channel between two processes. Pipe() returns a pair of connection objects — one for the parent and one for the child. For more than 2 processes, use Queue instead.6. Inter-Process Communication — Queue
Produced: data-0 Produced: data-1 Consumer-0: data-0 -> DATA-0 Consumer-1: data-1 -> DATA-1 ... Total results: 10
7. Manager — Shared State
Shared dict: {'key0': 0, 'key1': 20, 'key2': 40, 'key3': 60, 'key4': 80}
Shared list: ['key0:0', 'key1:10', 'key2:20', 'key3:30', 'key4:40']8. ProcessPoolExecutor (concurrent.futures)
Sequential: 54321 primes in 3.45s Parallel: 54321 primes in 0.92s Speedup: 3.75x
📝 Hindi Explanation
ProcessPoolExecutor is the high-level interface for multiprocessing. Use it like a Thread Pool, but with processes. Provides true speedup in CPU-bound tasks because each process has its own GIL.
9. Multiprocessing vs Multithreading Comparison
Sequential: 2.456s Threading: 2.398s (GIL limits benefit) Multiprocessing: 0.678s (true parallelism!) Speedup: 3.62x
10. Best Practices
Optimal process count: 8
Summary
| Tool | Best For |
|---|---|
Process | Manual control over single processes |
Pool.map() | Batch CPU work, uniform tasks |
Pool.starmap() | Multiple arguments per task |
Queue | Producer-consumer, task queues |
Pipe | Direct parent-child communication |
Value/Array | Simple shared primitives |
Manager | Complex shared data structures |
ProcessPoolExecutor | High-level API, futures |
Key Rule: Use multiprocessing for CPU-bound (calculations, image processing, ML training), and threading for I/O-bound (network requests, file I/O, database queries).
⚠️ Common Mistakes
1. ❌ if __name__ == "__main__" Guard Missing
Forgetting this on Windows is the most common mistake. Without it, infinite process spawning can occur (fork bomb effect) and the system will hang.
# ❌ WRONG — Windows pe crash karega
import multiprocessing
def worker():
print("Working...")
p = multiprocessing.Process(target=worker)
p.start() # RuntimeError on Windows!
# ✅ CORRECT
if __name__ == "__main__":
p = multiprocessing.Process(target=worker)
p.start()
p.join()2. ❌ Mutable Objects Directly Share Karna
Memory is not shared between processes. If you directly modify a list or dict, the changes won't be visible to other processes.
3. ❌ Lock Use Na Karna with Shared Memory
Modifying a shared Value or Array without a Lock creates race conditions. The counter's value will be unpredictable.
# ❌ WRONG — Race condition
counter = multiprocessing.Value('i', 0)
def bad_increment(counter):
for _ in range(1000):
counter.value += 1 # NOT atomic!
# ✅ CORRECT — Lock use karo
lock = multiprocessing.Lock()
def good_increment(counter, lock):
for _ in range(1000):
with lock:
counter.value += 14. ❌ Too Many Processes Create Karna
Creating more processes than CPU cores DECREASES performance due to context switching overhead. The optimal count is os.cpu_count().
# ❌ WRONG — 100 processes on 8-core machine
pool = multiprocessing.Pool(processes=100) # Overhead!
# ✅ CORRECT
pool = multiprocessing.Pool(processes=os.cpu_count())5. ❌ Lambda ya Local Functions Pass Karna
In multiprocessing, functions must be pickled to be sent to child processes. Lambda and nested functions cannot be pickled.
# ❌ WRONG — PicklingError
p = multiprocessing.Process(target=lambda: print("hi"))
# ✅ CORRECT — Top-level named function use karo
def greet():
print("hi")
p = multiprocessing.Process(target=greet)6. ❌ join() Call Na Karna
Without join(), the main process won't wait for the child to finish. Orphan processes will be created and resources will leak.
# ❌ WRONG — process orphan ho sakta hai
p = multiprocessing.Process(target=worker)
p.start()
# Program exits, process still running!
# ✅ CORRECT
p.start()
p.join() # Wait for completion7. ❌ Large Objects as Arguments Pass Karna
Arguments are sent to processes via pickle/serialization. If you pass a 100MB DataFrame, a copy is made for each process — massive memory waste!
✅ Key Takeaways
- 🔑 Multiprocessing = True Parallelism — Each process has its own GIL and memory space, so CPU-bound tasks get real speedup.
- 🔑
if __name__ == "__main__"— This guard is MANDATORY on Windows. Best practice is to use it everywhere regardless of OS.
- 🔑 Pool.map() for Batch Work — When you need to run the same function on multiple inputs, Pool is the cleanest and most efficient approach.
- 🔑 Shared Memory needs Lock — ALWAYS use a Lock when working with
ValueandArray, otherwise race conditions will occur.
- 🔑 Manager for Complex Data — Use
multiprocessing.Manager()to share complex objects like Dict, List, and Namespace between processes.
- 🔑 Queue for Producer-Consumer — Queue is the best option for safe communication between multiple producers and consumers.
- 🔑 ProcessPoolExecutor = Modern API — Part of the
concurrent.futuresmodule, offering a simple and clean interface with Future objects.
- 🔑 Optimal Processes = CPU Count — Creating more processes than
os.cpu_count()is counterproductive.
- 🔑 Pickling Constraint — Everything sent to child processes (functions, arguments) must be picklable. Lambda, closures, and complex objects will fail.
- 🔑 CPU-bound → Multiprocessing, I/O-bound → Threading — This is the golden rule. The wrong choice yields no performance benefit.
❓ FAQ
Q1: What is the difference between Multiprocessing and Multithreading?
Answer: In multiprocessing, each task runs in its own separate process with its own memory space and GIL. In multithreading, all threads share the same process memory. Multiprocessing gives true parallelism for CPU-bound work; threading is better for I/O-bound tasks.
Q2: How many processes should you put in a Pool?
Answer: Generally os.cpu_count() is optimal. If tasks involve some I/O, try cpu_count() + 1 or cpu_count() * 2. But more isn't always better — excess processes increase context switching overhead.
Q3: Can variables be shared directly between processes?
Answer: No! Each process has its own memory copy. To share data, you must explicitly use multiprocessing.Value, multiprocessing.Array, Manager objects, or shared_memory (Python 3.8+).
Q4: "PicklingError" kyun aata hai?
Answer: Multiprocessing sends arguments and functions to child processes by pickling (serializing) them. Lambda functions, nested functions, and some complex objects can't be pickled, hence they can't be used with multiprocessing.
Q5: What is the difference between fork and spawn start methods?
Answer: fork (Linux default) — creates an exact copy of the parent process, fast but unsafe with threads. spawn (Windows default) — starts a fresh Python interpreter and imports the module, slower but safer and more portable.
Q6: Is exception handling different in multiprocessing?
Answer: Yes! If an exception occurs in a child process, it doesn't automatically propagate to the parent. Pool.map() and ProcessPoolExecutor capture exceptions and re-raise them when you access results. For raw Process, you need explicit error handling.
Q7: Memory consumption is too high — what should I do?
Answer: Each process has its own memory copy. Solutions: (1) Pass large data as file paths; load within the process. (2) Use Pool.imap() or imap_unordered() for streaming results. (3) Use shared_memory for zero-copy access. (4) Reduce the number of processes.
Q8: What is a multiprocessing daemon process?
Answer: Setting p.daemon = True makes the process a daemon. Daemon processes terminate automatically when the main process exits. They're useful for background tasks that don't need explicit cleanup. But daemon processes cannot spawn child processes of their own.
🎯 Interview Questions
Q1: What is the GIL in Python and how does multiprocessing bypass it?
Answer: GIL (Global Interpreter Lock) is a mutex in the Python interpreter that ensures only one thread executes Python bytecode at a time. This prevents true thread-level parallelism for CPU-bound work. Multiprocessing bypasses it by creating separate processes, each with its own interpreter and GIL.
Q2: Pool.map() vs Pool.apply_async() — when to use which?
Answer: Pool.map() is a blocking call — results are only available after all tasks complete. Best when you have uniform tasks and need all results together. Pool.apply_async() returns immediately with an AsyncResult — best when tasks have varying durations and you want to process results as they arrive.
Q3: How can deadlock occur in multiprocessing? Give an example.
Answer: Deadlock occurs when two or more processes wait for each other's resources:
lock1 = multiprocessing.Lock()
lock2 = multiprocessing.Lock()
def process_a():
lock1.acquire()
time.sleep(0.1)
lock2.acquire() # Waits for lock2, held by process_b
def process_b():
lock2.acquire()
time.sleep(0.1)
lock1.acquire() # Waits for lock1, held by process_a
# DEADLOCK! Both will keep waiting for each other.Solution: Always acquire locks in same order across all processes, ya lock.acquire(timeout=5) use karo.
Q4: multiprocessing.Queue vs multiprocessing.Pipe — when to use which?
Answer: Pipe is only for 2 endpoints (parent ↔ child). It's fast because it's a direct connection. Queue supports multiple producers and consumers, is thread/process safe, but slightly slower. Use Pipe for simple bidirectional communication; Queue for complex architectures.
Q5: Are multiprocessing.Value and multiprocessing.Array thread-safe?
Answer: Individual operations (read/write) are atomic, but compound operations (read-modify-write like counter.value += 1) are NOT atomic. This is why you always need a Lock for compound operations on shared state.
Q6: What is Python 3.8+'s multiprocessing.shared_memory and when should you use it?
Answer: shared_memory.SharedMemory creates a named shared memory block that multiple processes can access directly without pickling/copying. Use for large NumPy arrays or binary data where serialization overhead is unacceptable.
Q7: What happens if a worker crashes in a process pool?
Answer: If using Pool.map() and a worker raises an exception, it's re-raised in the main process. The Pool keeps the remaining workers running. With apply_async(), the exception is stored in the AsyncResult — access it via .get(). Dead workers are replaced automatically.
Q8: How do you handle logging in multiprocessing?
Answer: Writing to the same log file from multiple processes is unsafe (file corruption). Solutions: (1) QueueHandler + QueueListener — all processes send log records to a queue, one listener writes them. (2) Separate log files per process. (3) logging.handlers.SocketHandler for centralized logging.
Q9: What is the role of the chunksize parameter in Pool.map()?
Answer: chunksize determines how many items are given to a worker in a single batch. By default, Pool divides items equally. Small chunks increase IPC overhead but improve load balancing. Large chunks reduce overhead but may cause uneven work distribution. Rule of thumb: len(data) / (pool_size * 4).
Q10: When should you use multiprocessing in real-world projects — give 3 practical examples.
Answer:
- Image Processing Pipeline — Resizing/compressing 1000 images. Each image is independent and CPU-intensive → Pool.map() is perfect.
- Data Science / ML — Feature engineering on a large dataset. Split DataFrame into chunks, process in parallel, merge results.
- Web Scraping + Processing — After downloading pages (threading), parsing HTML and NLP analysis (CPU-bound) → use multiprocessing for the parsing step.
Resized 150 images!
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Python Multiprocessing.
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, multiprocessing, python multiprocessing
Related Python Master Course Topics