Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
206: Sharing Data Between Processes
I've been working on a script to process a huge batch of high-resolution images, and since image processing is CPU-intensive, I decided to use the multiprocessing module to take advantage of all my CPU cores. I wanted a simple way to keep track of how many images were finished in real-time, so I started with the most obvious approach: a global counter.
The Global Variable Trap
Here is exactly how I started. I defined a global variable completed_count, spawned a few processes to do some "work," and then printed the total. It looked logically sound to me at the time.
import multiprocessing
import time
completed_count = 0
def process_image(image_id):
global completed_count
print(f"Processing image {image_id}...")
time.sleep(0.1) # Simulating work
completed_count += 1
if __name__ == "__main__":
processes = []
for i in range(5):
p = multiprocessing.Process(target=process_image, args=(i,))
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"Total images processed: {completed_count}")
When I ran this, I expected to see "Total images processed: 5". Instead, it printed "Total images processed: 0". Every single time. I stared at the code for a minute, thinking I'd missed a return statement or a scope issue, but the reality is more fundamental: processes do not share memory.
When you start a new process in Python, it gets its own entirely separate instance of the Python interpreter and its own memory space. The completed_count variable was copied into each child process. Each process incremented its own local copy of the counter, and when the process died, that local memory vanished. The parent process's counter never budged.
Giving Processes a Common Ground
To fix this, I need a way to tell Python, "Don't give these processes their own copy; give them a pointer to a piece of memory they can all see." For simple types, the multiprocessing module provides Value and Array. These are stored in shared memory outside the usual Python object heap.
Let's try swapping that global integer for a multiprocessing.Value.
import multiprocessing
import time
def process_image(image_id, counter):
print(f"Processing image {image_id}...")
time.sleep(0.1)
# We have to access the actual value through the .value attribute
counter.value += 1
if __name__ == "__main__":
# 'i' stands for a signed integer
completed_count = multiprocessing.Value('i', 0)
processes = []
for i in range(5):
p = multiprocessing.Process(target=process_image, args=(i, completed_count))
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"Total images processed: {completed_count.value}")
Now it works. It prints "5". But as a software engineer, this immediately makes me nervous. counter.value += 1 is not an atomic operation. It's actually three steps: read the value, add one, write it back. If two processes do the "read" step at the exact same millisecond, they'll both read "10", both increment it to "11", and both write "11" back—and we just lost a count. This is a classic race condition.
Ensuring Data Integrity
To prevent the processes from stepping on each other's toes, I need a Lock. A lock ensures that only one process can modify the shared value at a time. It's like a "talking stick" in a meeting; you can't speak (modify data) unless you're holding the stick.
import multiprocessing
import time
def process_image(image_id, counter, lock):
print(f"Processing image {image_id}...")
time.sleep(0.1)
with lock:
counter.value += 1
if __name__ == "__main__":
completed_count = multiprocessing.Value('i', 0)
lock = multiprocessing.Lock()
processes = []
for i in range(10):
p = multiprocessing.Process(target=process_image, args=(i, completed_count, lock))
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"Total images processed: {completed_count.value}")
By using with lock:, I'm guaranteeing that the read-modify-write cycle happens uninterrupted. It's a small performance hit, but it's the only way to ensure the numbers are actually correct when you're scaling up to dozens of processes.
When a Single Number Isn't Enough
What if I don't just want a count? What if I want to keep a list of which images failed, or a dictionary mapping image IDs to their processing time? Value and Array are too primitive for that.
This is where the Manager comes in. A Manager creates a server process that holds Python objects and allows other processes to manipulate them using proxies. It's more flexible than shared memory, though slightly slower because it involves inter-process communication (IPC) under the hood.
import multiprocessing
def process_image(image_id, results_dict):
# Simulating a result: 'Success' or 'Fail'
status = "Success" if image_id % 2 == 0 else "Fail"
results_dict[image_id] = status
if __name__ == "__main__":
with multiprocessing.Manager() as manager:
# Create a shared dictionary
shared_results = manager.dict()
processes = []
for i in range(5):
p = multiprocessing.Process(target=process_image, args=(i, shared_results))
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"Final Results: {shared_results}")
The Manager is essentially the "Swiss Army Knife" of shared data. If you need a shared list, shared dict, or even a shared queue, the Manager handles the heavy lifting of syncing that data across the process boundaries for you.
📋 Practical Task
Build a Multi-Process Resource Monitor
You are building a system that monitors the health of several simulated servers. Each "monitor" process should check a server and report back whether the server is 'Online' or 'Offline'.
- Create a
multiprocessing.Manager().dict()to store the results, where the key is the server name (e.g., "Server-1") and the value is the status. - Create a
multiprocessing.Value('i', 0)to track the total number of servers checked. - Spawn 6 processes. Each process should:
- Simulate a check using
time.sleep(random.random()). - Update the shared dictionary with a random status ('Online' or 'Offline').
- Increment the shared counter using a
multiprocessing.Lock()to ensure thread-safety.
- Simulate a check using
- Once all processes have joined, print the final dictionary of server statuses and the total count of servers checked.
There are no comments for now.