Python Multiprocessing: Many Helpers, One Big Job
The Kitchen Analogy 🍳
Imagine you’re making a huge feast for 100 guests. You have lots of dishes to prepare. If you cook alone, it takes forever. But what if you had 10 chefs, each with their own kitchen, working at the same time?
That’s multiprocessing in Python! Each chef is a process. Each kitchen is a separate workspace in memory. They all work together to finish the big job faster.
What is a Process?
A process is like a separate worker with its own:
- đź§ Brain (memory)
- 🍳 Kitchen (workspace)
- đź“‹ Recipe list (code to run)
Each process runs independently. If one chef burns a dish, the others keep cooking!
Why Not Just Threads?
Think of threads like helpers in the same kitchen. They share the same stove, fridge, and counter. Sometimes they bump into each other (that’s a “race condition”).
Processes are in separate kitchens. No bumping! Each has its own everything.
| Feature | Threads | Processes |
|---|---|---|
| Memory | Shared | Separate |
| Speed | Faster to start | Slower to start |
| Safety | Can conflict | Independent |
| Best for | Waiting tasks (I/O) | Heavy work (CPU) |
1. Process Creation: Hiring New Chefs
The Simplest Way
from multiprocessing import Process
def cook_dish(dish_name):
print(f"Cooking {dish_name}...")
# Create a process (hire a chef)
chef = Process(target=cook_dish,
args=("pasta",))
# Start the process
chef.start()
# Wait for it to finish
chef.join()
What happens?
- We create a new chef (Process)
- We tell them to make “pasta”
start()sends them to their kitchenjoin()waits until they’re done
Passing Arguments
def make_order(item, quantity):
print(f"Making {quantity} {item}s")
p = Process(
target=make_order,
args=("cookie", 12) # tuple!
)
p.start()
p.join()
Remember: args must be a tuple. Even for one item: args=("pasta",) with that comma!
2. Managing Processes: Running the Kitchen
Starting Multiple Chefs
from multiprocessing import Process
def cook(chef_id, dish):
print(f"Chef {chef_id}: {dish}")
# Create a team of chefs
chefs = []
dishes = ["soup", "salad", "bread"]
for i, dish in enumerate(dishes):
p = Process(target=cook,
args=(i, dish))
chefs.append(p)
# Start all at once
for p in chefs:
p.start()
# Wait for all to finish
for p in chefs:
p.join()
Checking Process Status
p = Process(target=cook_dish,
args=("steak",))
p.start()
# Is the chef still cooking?
print(p.is_alive()) # True or False
# What's their process ID?
print(p.pid) # Like a badge number
# Wait with timeout
p.join(timeout=5) # Wait max 5 secs
Naming Your Processes
p = Process(
target=cook_dish,
args=("pizza",),
name="Pizza-Chef" # Give a name!
)
print(p.name) # "Pizza-Chef"
Stopping a Process
p.terminate() # Stop immediately!
p.kill() # Force stop (Python 3.7+)
Warning: terminate() is like shouting “STOP!” mid-recipe. The dish won’t be finished!
3. Inter-Process Communication: Chefs Talking
Our chefs are in separate kitchens. How do they share information?
Method 1: Queue (The Order Window)
Think of a Queue like a restaurant order window. One chef puts orders in, another takes them out.
from multiprocessing import (
Process, Queue
)
def chef(q):
while True:
order = q.get() # Take order
if order == "DONE":
break
print(f"Making: {order}")
# Create the order window
orders = Queue()
# Start the chef
p = Process(target=chef,
args=(orders,))
p.start()
# Send orders
orders.put("burger")
orders.put("fries")
orders.put("DONE") # Signal to stop
p.join()
Queue Rules:
put()= add an itemget()= take an item (waits if empty)- Works like a line: first in, first out
Method 2: Pipe (Direct Phone Line)
A Pipe is like a phone between two chefs. They can talk back and forth!
from multiprocessing import (
Process, Pipe
)
def chef(conn):
order = conn.recv() # Listen
conn.send(f"{order} is ready!")
conn.close()
# Create phone connection
main_phone, chef_phone = Pipe()
p = Process(target=chef,
args=(chef_phone,))
p.start()
# Send order, get response
main_phone.send("pizza")
response = main_phone.recv()
print(response) # "pizza is ready!"
p.join()
Method 3: Value & Array (Shared Chalkboard)
Sometimes chefs need to share a single number or list. Like a shared chalkboard in the hallway!
from multiprocessing import (
Process, Value, Array
)
def count_orders(counter, items):
counter.value += 1
items[0] = 99 # Change first item
# 'i' means integer, 'd' means decimal
counter = Value('i', 0)
items = Array('i', [1, 2, 3])
p = Process(target=count_orders,
args=(counter, items))
p.start()
p.join()
print(counter.value) # 1
print(items[:]) # [99, 2, 3]
Comparison: Which to Use?
graph TD A["Need to Share Data?"] --> B{What kind?} B -->|Stream of items| C["Queue"] B -->|Two-way chat| D["Pipe"] B -->|Single value| E["Value"] B -->|List of numbers| F["Array"]
4. ProcessPoolExecutor: The Restaurant Manager
Managing many chefs manually is hard. What if we had a manager who handles everything?
That’s ProcessPoolExecutor! It:
- Creates a team of workers (pool)
- Assigns tasks automatically
- Collects results for you
The Easy Way: map()
from concurrent.futures import (
ProcessPoolExecutor
)
def cook(dish):
return f"{dish} is ready!"
dishes = ["soup", "salad", "steak"]
# Manager creates 3 workers
with ProcessPoolExecutor(
max_workers=3
) as manager:
# Cook all dishes in parallel
results = manager.map(cook, dishes)
for r in results:
print(r)
# "soup is ready!"
# "salad is ready!"
# "steak is ready!"
Submit Individual Tasks
from concurrent.futures import (
ProcessPoolExecutor
)
def slow_cook(dish):
import time
time.sleep(1)
return f"{dish} done!"
with ProcessPoolExecutor() as pool:
# Submit tasks one by one
future1 = pool.submit(slow_cook,
"roast")
future2 = pool.submit(slow_cook,
"pie")
# Get results when ready
print(future1.result())
print(future2.result())
as_completed: First Done, First Out
from concurrent.futures import (
ProcessPoolExecutor,
as_completed
)
def cook(dish):
import time
import random
time.sleep(random.uniform(0.5, 2))
return dish
dishes = ["soup", "pasta", "bread"]
with ProcessPoolExecutor() as pool:
futures = {
pool.submit(cook, d): d
for d in dishes
}
# Get results as they finish
for future in as_completed(futures):
dish = futures[future]
result = future.result()
print(f"{dish} finished!")
Error Handling
def risky_cook(dish):
if dish == "disaster":
raise ValueError("Burned!")
return f"{dish} OK"
with ProcessPoolExecutor() as pool:
future = pool.submit(risky_cook,
"disaster")
try:
result = future.result()
except ValueError as e:
print(f"Oops: {e}")
Quick Reference Table
| Task | Code |
|---|---|
| Create process | Process(target=fn, args=()) |
| Start | p.start() |
| Wait | p.join() |
| Check alive | p.is_alive() |
| Get PID | p.pid |
| Stop | p.terminate() |
| Queue send | q.put(item) |
| Queue receive | q.get() |
| Pipe send | conn.send(data) |
| Pipe receive | conn.recv() |
| Shared int | Value('i', 0) |
| Shared list | Array('i', [1,2,3]) |
| Pool map | pool.map(fn, items) |
| Pool submit | pool.submit(fn, arg) |
| Get result | future.result() |
The Big Picture
graph TD A["Main Program"] --> B["Create Processes"] B --> C["Process 1"] B --> D["Process 2"] B --> E["Process 3"] C --> F["Do Work"] D --> F E --> F F --> G["Communicate via Queue/Pipe"] G --> H["Collect Results"] H --> I["Done!"]
When to Use Multiprocessing?
âś… Use it when:
- Heavy calculations (math, images)
- Processing many files
- Tasks that don’t share data often
❌ Don’t use when:
- Waiting for web requests (use threads)
- Tasks need to share lots of data
- Starting many short tasks (overhead)
You Did It! 🎉
You now understand:
- Process Creation - Making new workers
- Managing Processes - Starting, stopping, waiting
- Communication - Queue, Pipe, shared memory
- ProcessPoolExecutor - The easy manager
Like running a kitchen with many chefs, multiprocessing lets your Python code do many things at once. Each process has its own space, works independently, and together they get the big job done faster!
Now go build something amazing! 🚀
