Understanding Operating System Processes
An operating system process represents an active instance of an executing program. Each process is granted isolated memory space, a unique process identifier (PID), and scheduled CPU time slices. Python’s multiprocessing module enables developers to bypass the Global Interpreter Lock (GIL) by spawning separate OS-level processes for true parallel execution.
Spawning and Managing Worker Processes
Creating a parallel task involves instantiating the Process class from the multiprocessing package. The constructor accepts a target callable and an optional tuple of arguments. Calling start() initiates the execution in a separate memory space.
Asynchronous Execution With out Synchronization
When the parent script does not wait for child processes, the main thread proceeds immediately. The operating system schedules the child independently, which often results in interleaved or out-of-order console output.
import time
from multiprocessing import Process
def background_task(identifier):
for step in range(3):
print(f"Worker {identifier} - Step {step} initiated")
time.sleep(0.5)
print(f"Worker {identifier} - Step {step} completed")
if __name__ == "__main__":
worker = Process(target=background_task, args=("Alpha",))
worker.start()
print("Main thread continues immediately...")
Synchronizing Execution with join()
The join() method blocks the calling thread until the target process terminates. This synchronization ensures that subsequent logic executes only after the child process finishes its assigned workload.
import time
from multiprocessing import Process
def background_task(identifier):
for step in range(3):
print(f"Worker {identifier} - Step {step} initiated")
time.sleep(0.5)
print(f"Worker {identifier} - Step {step} completed")
if __name__ == "__main__":
worker = Process(target=background_task, args=("Alpha",))
worker.start()
worker.join()
print("Main thread resumes after child completion.")
Forceful Termination via terminate()
If a child process enters an unresponsive state or requires immediate cancellation, terminate() sends a SIGTERM signal. Always follow this with join() to guarantee proper cleanup of underlying OS resources and prevent zombei processes.
import time
from multiprocessing import Process
def long_running_task(name):
for _ in range(5):
print(f"Processing {name}...")
time.sleep(2)
if __name__ == "__main__":
task = Process(target=long_running_task, args=("HeavyJob",))
task.start()
time.sleep(1)
task.terminate()
task.join()
print("Process forcibly stopped and resources released.")
Inter-Process Communication Using Queues
Because each process maintains isolated memory, sharing standard Python variables directly is impossible. The Queue class provides a thread- and process-safe FIFO buffer for exchanging data. It supports configurable capacity limits and configurable blocking behavior.
qsize(): Returns the approximate number of buffered items.empty()/full(): Boolean status indicators for queue state.get(block=True, timeout=None): Removes and returns the next item. Blocks indefinitely if empty unless a timeout is specified, which raisesqueue.Emptyupon expiration.put(item, block=True, timeout=None): Inserts data into the buffer. Blocks if the queue reaches capacity unless a timeout is provided, triggeringqueue.Fullwhen exceeded.get_nowait()/put_nowait(item): Non-blocking variants that raise exceptions immediately when the queue state prevents the operation.
Producer-Consumer Implementation
import time
import random
from multiprocessing import Process, Queue
def data_producer(queue_handle):
for batch in range(4):
payload = f"Packet-{batch}"
print(f"[Producer] Generating {payload}")
queue_handle.put(payload)
time.sleep(random.uniform(0.2, 0.5))
def data_consumer(queue_handle):
while True:
if not queue_handle.empty():
item = queue_handle.get()
print(f"[Consumer] Processed {item}")
time.sleep(0.3)
else:
break
if __name__ == "__main__":
shared_buffer = Queue()
p1 = Process(target=data_producer, args=(shared_buffer,))
p2 = Process(target=data_consumer, args=(shared_buffer,))
p1.start()
p2.start()
p1.join()
p2.join()
print("Exchange complete.")
Integrating Queues with Process Pools
When distributing work across multiple workers using Pool, standard Queue instances cannot be pickled and passed across process boundaries. Attempting to do so raises a RuntimeError. The correct approach is to instantiate a managed queue through multiprocessing.Manager(), which creates a proxy object that safely routes data between separate memory spaces.
Pool-Based Workflow with Managed Queue
import os
import time
from multiprocessing import Manager, Pool
def pool_writer(shared_queue):
for idx in range(5):
shared_queue.put(f"Task-{idx}")
print(f"Pool Worker {os.getpid()} enqueued Task-{idx}")
time.sleep(0.4)
def pool_reader(shared_queue):
active = True
while active:
try:
data = shared_queue.get(timeout=1.0)
print(f"Pool Worker {os.getpid()} consumed {data}")
time.sleep(0.6)
except Exception:
active = False
if __name__ == "__main__":
manager_obj = Manager()
sync_queue = manager_obj.Queue()
worker_pool = Pool(processes=3)
print(f"Parent PID: {os.getpid()}")
worker_pool.apply_async(pool_writer, args=(sync_queue,))
worker_pool.apply_async(pool_reader, args=(sync_queue,))
worker_pool.close()
worker_pool.join()
print(f"Parent PID: {os.getpid()} finished.")