
مقدمه
در برنامهنویسی با multiprocessing، هر پردازه (process) فضای حافظهی جداگانهی خودش را دارد؛ بنابراین برخلاف تردها، نمیتوانند بهسادگی یک متغیر مشترک را ببینند. برای همین به «ارتباط بین پردازهای» (IPC) نیاز داریم. پایتون چند روش برای این کار فراهم کرده که هرکدام ویژگیها و محدودیتهای خودش را دارد. در ادامه سه روش اصلی را با مثال بررسی میکنیم و در پایان با یک جدول مقایسه میکنیم.
۱. استفاده از Queue (صف)
Queue یکی از پرکاربردترین روشهای ارتباط بین پردازههاست. صفِ ماژول multiprocessing بهصورت امن (thread/process-safe) عمل میکند؛ یعنی میتوانید بدون نگرانی از همگامسازی دستی، از آن برای ارسال و دریافت داده بین پردازهها استفاده کنید. یک پردازه داده را put میکند و پردازهی دیگر آن را get:
from multiprocessing import Process, Queue
def producer(queue):
for i in range(5):
queue.put(i)
queue.put(None) # sentinel: signals end of data
def consumer(queue):
while True:
item = queue.get()
if item is None:
break
print(f"Consumed: {item}")
if __name__ == "__main__":
queue = Queue()
producer_process = Process(target=producer, args=(queue,))
consumer_process = Process(target=consumer, args=(queue,))
producer_process.start()
consumer_process.start()
producer_process.join()
consumer_process.join()مقدار ویژهی None در انتها، به مصرفکننده میگوید «دیگر دادهای نمیآید، حلقه را تمام کن». توجه کنید که کل کد باید داخل if __name__ == "__main__": باشد؛ این در multiprocessing ضروری است.
مثال واقعی: توزیع کار بین چند کارگر
رایجترین کاربرد واقعیِ Queue، تقسیم یک دسته کار سنگین (CPU-bound) بین چند پردازهی کارگر است تا روی چند هستهی پردازنده موازی اجرا شوند. مثلاً پردازش دستهای از تصاویر:
from multiprocessing import Process, Queue
def worker(task_queue, result_queue):
while True:
job = task_queue.get()
if job is None:
break
result_queue.put((job, job * job)) # pretend this is heavy CPU work
if __name__ == "__main__":
task_queue = Queue()
result_queue = Queue()
jobs = range(10)
for j in jobs:
task_queue.put(j)
workers = [Process(target=worker, args=(task_queue, result_queue)) for _ in range(3)]
for w in workers:
task_queue.put(None) # one stop signal per worker
for w in workers:
w.start()
for w in workers:
w.join()
while not result_queue.empty():
print(result_queue.get())۲. استفاده از Pipe (لوله)
Pipe یک کانال دوطرفه بین دو پردازه است. هر دو سر لوله میتوانند داده بفرستند و بگیرند، اما نسبت به Queue سطحپایینتر است و باید خودتان جریان داده را مدیریت کنید تا دچار بنبست نشوید. در مثال زیر، فرستنده پنج عدد و در پایان یک None (بهعنوان علامت پایان) میفرستد و گیرنده تا رسیدن None میخواند:
from multiprocessing import Process, Pipe
def sender(conn):
for i in range(5):
conn.send(i)
print(f"send: {i}")
conn.send(None) # sentinel to signal the end
conn.close()
def receiver(conn):
while True:
item = conn.recv()
if item is None:
break
print(f"Received: {item}")
conn.close()
if __name__ == "__main__":
sender_conn, receiver_conn = Pipe()
sender_process = Process(target=sender, args=(sender_conn,))
receiver_process = Process(target=receiver, args=(receiver_conn,))
sender_process.start()
receiver_process.start()
sender_process.join()
receiver_process.join()نکته: استفاده از یک علامت پایان (اینجا None) امنتر از تکیه بر conn.poll() است؛ چون poll() فقط میگوید «همین حالا دادهای هست یا نه» و اگر گیرنده کمی زودتر از فرستنده اجرا شود، ممکن است حلقه پیش از رسیدن دادهها تمام شود.
۳. استفاده از متغیرهای مشترک (Value و Array)
گاهی بهجای فرستادن پیام، میخواهیم چند پردازه به یک قطعهی حافظهی مشترک دسترسی داشته باشند. multiprocessing برای این کار Value (یک مقدار تکی) و Array (یک آرایه) را فراهم میکند که در حافظهی مشترک قرار میگیرند و همهی پردازهها تغییراتشان را میبینند:
from multiprocessing import Process, Array, Value
def square_numbers(numbers, result, first_square):
for i in range(len(numbers)):
result[i] = numbers[i] ** 2
first_square.value = numbers[0] ** 2 # set the shared Value
if __name__ == "__main__":
shared_array = Array('i', range(5)) # 'i' = array of integers
shared_value = Value('i', 0) # 'i' = a single integer
numbers = [1, 2, 3, 4, 5]
process = Process(target=square_numbers, args=(numbers, shared_array, shared_value))
process.start()
process.join()
print("Shared Array:", list(shared_array)) # [1, 4, 9, 16, 25]
print("Shared Value:", shared_value.value) # 1حرف 'i' نوع داده را مشخص میکند (اینجا عدد صحیح). نکتهی مهم: اگر چند پردازه همزمان بخواهند یک Value یا Array مشترک را تغییر دهند، دوباره با خطر Race Condition روبهرو میشویم؛ در آن حالت باید از multiprocessing.Lock برای محافظت استفاده کنید (که در درس جداگانهی قفل در multiprocessing بررسی شده است).
جدول مقایسه
| روش | تعداد طرفین | ایمن بهصورت خودکار؟ | مناسب برای |
|---|---|---|---|
Queue | چند تولیدکننده/مصرفکننده | بله (process-safe) | توزیع کار و جریان پیامها |
Pipe | دقیقاً دو پردازه | خیر (مدیریت دستی) | ارتباط سریع و سادهی دو طرفه |
Value / Array | چند پردازه | خیر (به Lock نیاز دارد) | اشتراک مستقیم چند مقدار در حافظه |
جمعبندی
- چون پردازهها حافظهی مشترک ندارند، برای تبادل داده به IPC نیاز داریم.
Queue: امن و پرکاربرد، عالی برای توزیع کار بین چند کارگر.Pipe: کانال دوطرفهی سریع بین دو پردازه؛ سطحپایینتر و نیازمند مدیریت دستی.Value/Array: اشتراک مستقیم داده در حافظه؛ برای نوشتنِ همزمان بهLockنیاز دارند.