How-To Use Multiprocessing in Python

How-To Use Multiprocessing in Python
1. What is Multiprocessing?

In simple terms, multiprocessing is a way to run multiple parts of your Python program at the exact same time by using separate CPU cores.

Normally, a Python script runs on a single CPU core. Even if you use threading, Python’s Global Interpreter Lock (GIL) prevents threads from running Python code on more than one core simultaneously.

Multiprocessing bypasses the GIL entirely by creating new, independent processes. Each process gets its own Python interpreter and its own memory space. It’s like running several different Python scripts at once, all of which can work together.

Analogy:

  • Threading (single-core): One chef (your CPU core) quickly switching between three different tasks (e.g., chopping, stirring, plating). It looks like parallel work, but it’s just fast switching.
  • Multiprocessing (multi-core): Hiring three separate chefs (your CPU cores), each working in their own station, all at the same time. This is true parallel work.

2. Why Use Multiprocessing?

There are two primary reasons to use multiprocessing: speed and responsiveness/stability.

  • True Parallelism (Speed)
    If you have a CPU-heavy task (like complex math, video processing, or searching large datasets) and a multi-core processor, multiprocessing allows you to use all those cores. You can split the work among multiple processes and get the job done in a fraction of the time.
  • Responsiveness & Stability (Decoupling)
    This is the most critical reason for real-world applications, especially in robotics and automation.
    A perfect example is a robot vision system that must perform three tasks at different speeds:
    • Camera Task (Very Fast): Needs to run at 30-60 FPS to provide a smooth video feed and get the most recent data.
    • Detection Task (CPU-Heavy): Complicated image analysis might take 100-200ms per frame, meaning it can only run at 5-10 FPS.
    • Robot Task (Slow & Critical): Physical movements take several seconds, and the robot hardware requires a constant, fast “heartbeat” signal to stay connected.
  • The Problem Without Multiprocessing:
    If you run this in a single loop, your entire application is only as fast as its slowest part.
    • The camera feed will freeze for several seconds every time the robot moves.
    • The robot’s “heartbeat” signal will be blocked by the 200ms detection task. Many industrial cobots (like a Universal Robot) will see this delay as a connection loss and trigger a protective stop, halting your entire operation.
  • The Multiprocessing Solution:
    You “decouple” these tasks by putting each in its own process.
    • Process 1 (Camera): Runs at 60 FPS, completely unaffected by anything else.
    • Process 2 (Detection): Runs at 10 FPS, taking all the time it needs.
    • Process 3 (Robot): Runs its own loop, sending fast heartbeat signals and handling slow movements, all while remaining perfectly stable.

3. How Does It Work?
The key concept to understand is isolation.

Because each process has its own memory, they cannot share variables directly. If Process A changes a variable x, Process B will not see that change. This is by design and prevents data corruption.

So, how do they work together? They must communicate by passing messages through “channels.” The most common and robust channel is a Queue.

  • A multiprocessing.Queue is like a process-safe “mailbox.”
  • One process can put() an item (like a video frame or a command) into the queue.
  • Another process can get() that item, waiting if the queue is empty.

4. How to Apply It: The 3-Process Automation Pattern
This is a powerful and common pattern for robotics. We create a “pipeline” where data flows from one worker to the next.

Step 1: Define the Architecture
First, map out your independent tasks and the data that flows between them.

  • Process 1: camera_worker
    • Job: Reads from the camera as fast as possible.
    • Outputs: Sends video frames to the detection_worker.
  • Process 2: detection_worker
    • Job: Performs heavy CPU analysis.
    • Inputs: Receives video frames from the camera_worker.
    • Outputs: Sends detection results (like coordinates and angle) to the robot_worker.
  • Process 3: robot_worker
    • Job: Manages the robot’s state and physical movements.
    • Inputs: Receives detection results from the detection_worker and user commands (like ‘start’/‘stop’) from the main process.

Step 2: Create Queues and Events
In your main script, you set up the “mailboxes” and a “stop button” that all processes can share.

Python

from multiprocessing import Process, Queue, Event

if __name__ == "__main__":
    # 1. Create the "mailboxes"
    frame_q = Queue(maxsize=2)         # Camera -> Detection
    detection_q = Queue(maxsize=5)     # Detection -> Robot
    command_q = Queue(maxsize=5)       # Main -> Robot
    
    # 2. Create the "stop button"
    stop_event = Event()

Step 3: Write the Worker Functions
Each worker is a function that runs an infinite loop, which only ends when the stop_event is set.

Python

def camera_worker(frame_q, stop_event):
    # cam = initialize_camera()...
    while not stop_event.is_set():
        # frame = cam.read()...
        try:
            # Send frame to detection_worker
            frame_q.put_nowait(frame) 
        except queue.Full:
            pass # Skip frame if detection is lagging

def detection_worker(frame_q, detection_q, stop_event):
    while not stop_event.is_set():
        try:
            # Wait for a new frame from camera_worker
            frame = frame_q.get(timeout=1)
            
            # ... run heavy_analysis(frame) ...
            # result = { 'x': 100, 'y': 250, 'angle': 0.5 }
            
            # Send results to robot_worker
            detection_q.put(result)
        except queue.Empty:
            continue # No new frame, just loop again

def robot_worker(detection_q, command_q, stop_event):
    robot_state = "IDLE"
    # robot = initialize_robot()...
    
    while not stop_event.is_set():
        # 1. Check for user commands
        try:
            cmd = command_q.get_nowait()
            if cmd == 'toggle_state':
                robot_state = "RUNNING" if robot_state == "IDLE" else "IDLE"
        except queue.Empty:
            pass

        # 2. Run the main robot logic
        if robot_state == "RUNNING":
            try:
                # Check for new work
                result = detection_q.get_nowait()
                
                # ... tell robot to move to result['x'], result['y'] ...
                # This part takes 5 seconds, but it's okay,
                # because the camera and detection are still running!
                
            except queue.Empty:
                pass # No new detections

Step 4: The Main Orchestrator
The if name == “main”: block is the “orchestrator.” It starts the workers and listens for user input.

Python

if __name__ == "__main__":
    # (Queues and Event created here...)
    
    # 1. Create the list of workers
    workers = [
        Process(target=camera_worker, args=(frame_q, stop_event)),
        Process(target=detection_worker, args=(frame_q, detection_q, stop_event)),
        Process(target=robot_worker, args=(detection_q, command_q, stop_event))
    ]

    # 2. Start all workers
    print("Starting all workers...")
    for w in workers:
        w.start()

    # 3. Run the main UI loop (e.g., OpenCV window)
    try:
        while True:
            # ... code to show a display window ...
            # key = cv.waitKey(1) & 0xFF

            if key == ord('q'): # 'q' to quit
                print("Shutdown signal sent.")
                stop_event.set()
                break
            
            if key == ord('g'): # 'g' to toggle robot
                print("Toggling robot state.")
                command_q.put('toggle_state')

    finally:
        # 4. Clean up
        for w in workers:
            w.join() # Wait for each process to finish
        print("All processes have shut down.")