Python Concurrent Programming with Multiprocessing

Operating System Role

An operating system (OS) sits between hardware and application software, consisting of a kernel and system interfaces. The OS exclusively controls hardware resources, while apps interact via OS-provided APIs. Key functions include abstracting low-level hardware operations and managing orderly resource competition.

Multiprogramming Technique

To break the single-task execution bottleneck, the OS uses switching with state preservation:

  • Switch Triggers: IO blocking, execution timeout, or higher-priority task arrival
  • State Preservation: Saves a process’s context before switching to resume later
  • Implementation: Space multiplexing (loading multiple apps into memory) and time multiplexing (CPU time slicing)
  • Key Notes: Processes are physically isolated for safety; IO-bound tasks see efficiency gains, while CPU-bound tasks may see overhead from switcihng.

Process vs Program

A program is static source code; a process is a running program instance managed by the OS.

Process States & Execution Modes

  • Serial: Single-task sequential execution
  • Concurrent: Multiprocessing with single CPU + time multiplexing
  • Parallel: True simultaneous execution on multi-core CPUs
  • Blocked: Process waiting on IO operations

Process Identification

Use os.getpid() and os.getppid() to get a process’s ID and parent process ID.

Multiprocessing Implementation in Python

Two Methods to Create Processes

Method 1: Instantiate Process Class

from multiprocessing import Process

def worker(worker_id):
    print(f"Worker {worker_id} is running")

if __name__ == '__main__':
    p = Process(target=worker, args=('p01',))
    p.start()
    print("Main process completes")

Method 2: Inherit from Process Class

from multiprocessing import Process

class CustomWorker(Process):
    def __init__(self, worker_name):
        super().__init__()
        self.worker_name = worker_name
    
    def run(self):
        print(f"CustomWorker {self.worker_name} is executing")

if __name__ == '__main__':
    w = CustomWorker('w02')
    w.start()

Critical Notes: Windows requires process creation under __main__ to avoid recursive forking.

Memory Isolation Between Processes

from multiprocessing import Process
import time

initial_value = 1000

def modify_value():
    time.sleep(2)
    global initial_value
    initial_value = 0
    print(f"Child process value: {initial_value}")

if __name__ == '__main__':
    print(f"Main process initial value: {initial_value}")
    p = Process(target=modify_value)
    p.start()
    p.join()
    print(f"Main process final value: {initial_value}")  # Output remains 1000

Process Methods and Attributes

  • start(): Sends a request to the OS to start a child process
  • join(): Blocks the parent until the child completes
  • terminate(): Sends a termination signal to the child
  • is_alive(): Returns the child’s active status
  • name/pid/exitcode: Process metadata

Daemon Processes

A daemon process monitors another process and terminates when the monitored process completes. It cannot spawn sub-processes.

from multiprocessing import Process
import time

def monitor():
    print("Daemon process running...")
    time.sleep(4)
    print("Daemon process exiting...")

if __name__ == '__main__':
    p = Process(target=monitor)
    p.daemon = True
    print("Main process starting...")
    p.start()
    time.sleep(2)
    print("Main process completes")

Process Safety Issues

Concurrency leads to race conditions when accessing shared resources. Use a mutex lock to serialize critical sections.

from multiprocessing import Process, Lock
import json
import time

def check_tickets(buyer, db_file):
    with open(db_file, 'r') as f:
        data = json.load(f)
    print(f"{buyer} checks tickets: {data['remaining']}")

def buy_ticket(buyer, db_file, lock):
    lock.acquire()
    time.sleep(0.5)
    with open(db_file, 'r') as f:
        data = json.load(f)
    if data['remaining'] > 0:
        data['remaining'] -= 1
        with open(db_file, 'w') as f:
            json.dump(data, f)
        print(f"{buyer} successfully bought a ticket")
    else:
        print(f"{buyer}: Tickets sold out")
    lock.release()

def ticket_task(buyer, db_file, lock):
    check_tickets(buyer, db_file)
    buy_ticket(buyer, db_file, lock)

if __name__ == '__main__':
    with open('ticket_db.json', 'w') as f:
        json.dump({'remaining': 2}, f)
    lock = Lock()
    buyers = ['Alice', 'Bob', 'Charlie']
    processes = [Process(target=ticket_task, args=(b, 'ticket_db.json', lock)) for b in buyers]
    for p in processes:
        p.start()
    for p in processes:
        p.join()

Inter-Process Communication (IPC)

Processes are memory-isolated; use these methods for communication:

  1. Shared files (disk-based, low speed)
  2. Shared memory (fast, limited size)
  3. Pipes (unidirectional, OS-encapsulated)
  4. Sockets (local/remote, network-based)

Example with Manager for shared memory:

from multiprocessing import Process, Manager, Lock

def update_counter(shared_dict):
    shared_dict['count'] += 1
    print(f"Child process counter: {shared_dict['count']}")

if __name__ == '__main__':
    with Manager() as manager:
        shared_data = manager.dict({'count': 10})
        print(f"Before update: {shared_data['count']}")
        lock = Lock()
        p = Process(target=update_counter, args=(shared_data,))
        p.start()
        p.join()
        print(f"After update: {shared_data['count']}")

Queue

A queue implements FIFO behavior. Use Queue from multiprocessing:

from multiprocessing import Process, Queue
import time
import random

def baker(q):
    for i in range(1, 6):
        time.sleep(random.randint(1, 2))
        bun = f"Steamed Bun #{i}"
        q.put(bun)
        print(f"Baker made {bun}")

def eater(q):
    for i in range(5):
        bun = q.get()
        time.sleep(random.randint(1, 2))
        print(f"Eater ate {bun}")

if __name__ == '__main__':
    q = Queue()
    p1 = Process(target=baker, args=(q,))
    p2 = Process(target=eater, args=(q,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()

JoinableQueue

Extends Queue with task_done() and join() to track completion of queue items:

from multiprocessing import Process, JoinableQueue
import time
import random

def baker(q):
    for i in range(1, 6):
        time.sleep(random.randint(1, 2))
        bun = f"Steamed Bun #{i}"
        q.put(bun)
        print(f"Baker made {bun}")

def eater(q):
    while True:
        bun = q.get()
        time.sleep(random.randint(1, 2))
        print(f"Eater ate {bun}")
        q.task_done()

if __name__ == '__main__':
    q = JoinableQueue()
    p_baker = Process(target=baker, args=(q,))
    p_eater = Process(target=eater, args=(q,))
    p_eater.daemon = True
    p_baker.start()
    p_eater.start()
    p_baker.join()
    q.join()
    print("All buns consumed")

Tags: python multiprocessing Concurrency Parallel Programming Process Communication

Posted on Sat, 15 Aug 2026 16:19:34 +0000 by FourthChapter