در این مقاله قصد داریم به تفصیل دربارۀ مبحث Multi Processing (پردازش موازی) در زبان برنامهنویسی پایتون صحبت کرده و با تفاوتهای مابین Process و Thread در قالب مثالهای کاربردی آشنا شویم.
تفاوت Process و Thread چیست؟
Process در واقع به برنامهای گفته میشود که اصطلاحاً Execute شده و از حالت Deactivate (غیرفعال) به حالت Activate (فعال) تبدیل شده باشد اما Thread در حقیقت جزئی از یک Process است؛ به عبارت دیگر، هر Process ممکن است از چندین Thread تشکیل شده باشد و حداقل شامل یک Thread میباشد که به آن Main Thread گفته میشود (به طور کلی، از Thread به این منظور استفاده میشود تا برخی از دستورالعملها در یک برنامه را به صورت Concurrent یا موازی اجرا کنیم).
همانطور که در یک برنامه میتوانیم از تِرِدهای مختلف -زیرمجموعۀ یک پراسس- برای اجرای دستورالعملهای برنامه استفاده کنیم، از پراسسهای مختلف هم میتوان استفاده کرد که در این صورت به مورد اول Multi Threading و به دومی Multi Processing گفته میشود که در ادامه به برخی از مهمترین تفاوتهای آنها اشاره خواهیم کرد.
تفاوتهای Multi Threading و Multi Processing
در Multi Threading، کلیۀ تِرِدها از یک حافظۀ مشترک استفاده میکنند که متعلق به پراسس مربوطه میباشند اما در Multi Processing هر پراسس از حافظۀ مخصوص به خود استفاده میکند.
همچنین در Multi Threading، هر تِرِد با استفاده از متدهای درون برنامه مثل ()notify() ،wait و ... با تِرِدهای دیگر ارتباط برقرار میکند ولی در Multi Processing هر پراسس با استفاده از مکانیزم IPC با پراسسهای دیگر ارتباط برقرار میکند (IPC که مخفف واژگان Inter Process Communication است، شامل روشهای مختلفی میشود که از معروفترین آنها میتوان به Socket و Pipe اشاره کرد).
در نهایت هم باید گفت که ساختن یک Process جدید از یک Thread، کندتر انجام شده و شامل هزینۀ -زمانی- بیشتری میشود.
Multi Processing در پایتون
اجازه بدهید با یک مثال، مبحث مالتی پراسسینگ در پایتون را شروع کنیم. قطعه کد زیر را در نظر بگیرید:
from multiprocessing import Process
def greeting():
print 'hello world'
if __name__ == '__main__':
p = Process(target=greeting)
p.start()
p.join()
قطعه کد بالا به صورت معمول اجرا میشود تا وقتی به خط ۷ و ۸ میرسد. در اینجا، علاوه بر پراسسی که برنامۀ اصلی در آن در حال اجرا است، یک پراسس جدید ایجاد میشود که فانکشن greeting را اجرا میکند؛ بنابراین در خط ۸ دو پراسس همزمان وجود دارد که یکی همان پراسس اصلی مربوط به برنامه است و دیگری در حین برنامه و در خط ۷ ایجاد و در خط ۸ اجرا شده است.
در خط ۹ از متد join بدین منظور استفاده شده است تا پراسس اصلی پس از به انتها رسیدن، اصطلاحاً Exit نشود و منتظر اتمام کار پراسس دیگر باشد. بنابراین هرگاه این متد روی یک پراسس ران شود، پراسس اصلی روی همان خط متوقف میشود تا کار پراسس تمام شده سپس به کار خود ادامه میدهد.
همانطور که دیده شد، برای ایجاد یک پراسس دلخواه در برنامه، از ماژول multiprocessing استفاده کرده و از طریق آن کلاس Process را ایمپورت کردیم. کلاس Process در کانستراکتور خود پارامتر target را حتماً باید داشته باشد زیرا فانکشنی که به آن پاس داده میشود را باید در target قرار دهیم. این کانستراکتور علاوه بر target، پارامتر مهم دیگری به نام args که از جنس tuple است را نیز میگیرد که همان آرگومانهای تابع میباشد. برای درک بهتر این موضوع، به مثال زیر توجه کنید:
from multiprocessing import Process
def square(numbers):
for x in numbers:
print('%s squared is %s' % (x, x**2))
if __name__ == '__main__':
numbers = [43, 50, 5, 98, 34, 35]
p = Process(target=square, args=(numbers,))
p.start()
p.join
print "Done"
#result
Done
43 squared is 1849
50 squared is 2500
5 squared is 25
98 squared is 9604
34 squared is 1156
35 squared is 1225
💎 آشنایی با دیتا تایپ تاپل در پایتون
در اینجا متغیر numbers را در پارامتر args قرار دادیم و به تابع square پاس دادیم چرا که تابع مذکور یک پارامتر دریافت میکرد و این در حالی است که حتی در برنامۀ خود میتوانیم چندین پراسس نیز ایجاد کنیم. برای مثال داریم:
from multiprocessing import Process
def square(numbers):
for x in numbers:
print('%s squared is %s' % (x, x**2))
def is_even(numbers):
for x in numbers:
if x % 2 == 0:
print('%s is an even number ' % (x))
if __name__ == '__main__':
numbers = [43, 50, 5, 98, 34, 35]
p1 = Process(target=square, args=(numbers,))
p2 = Process(target=is_even, args=(numbers,))
p1.start()
p2.start()
p1.join()
p2.join()
print "Done"
#result
43 squared is 1849
50 squared is 2500
5 squared is 25
98 squared is 9604
34 squared is 1156
35 squared is 1225
50 is an even number
98 is an even number
34 is an even number
Done
پراسسها چگونه با هم ارتباط برقرار میکنند؟
در اینجا با دو روش عمدهای که پراسسها در پایتون با یکدیگر ارتباط برقرار میکنند آشنا میشویم که یکی Queue است و دیگری Pipe نام دارد.
1. Queue: در ماژول multiprocessing یک کلاس به اسم Queue وجود دارد که فرق آن با ماژول Queue که اصطلاحاً Bulit-In پایتون است در این میباشد که در اینجا عناصری که به صف اضافه شده و یا کم میشوند در همۀ پراسسها دردسترس هستند و آبجکت Queue اصطلاحاً Share شده است. برای استفاده از Queue به صورت زیر عمل میکنیم:
import multiprocessing
def is_even(numbers, q):
for n in numbers:
if n % 2 == 0:
q.put(n)
if __name__ == "__main__":
q = multiprocessing.Queue()
p = multiprocessing.Process(target=is_even, args=(range(20), q))
p.start()
p.join()
while q:
print(q.get())
# result
0
2
4
6
8
10
12
14
16
18
در پراسس ایجاد شده در خط ۱۱ برنامه، اعدادی که به تابع is_even پاس داده میشوند طی فرایندی به صورت تکتک به صف ایجاد شده اضافه میشوند و از آنجایی که پراسس اصلی برنامه نیز به آن صف دسترسی همزمان دارد، بنابراین به محض پُر شدن صف در پراسس اصلی، صف دوباره خالی میشود.
2. Pipe: یکی دیگر از راههای ارتباطی بین پراسسها، Pipe است. در این روش، شما از یک حافظۀ مشترک بر خلاف مدل Queue استفاده نمیکنید بلکه به صورت مستقیم به یک پراسس دیگر، سیگنال میفرستید. برای درک بهتر این موضوع، مثال زیر را مد نظر قرار میدهیم:
from multiprocessing import Process, Pipe
def f(conn):
conn.send(['hello world'])
conn.close()
if __name__ == '__main__':
parent_conn, child_conn = Pipe()
p = Process(target=f, args=(child_conn,))
p.start()
print parent_conn.recv()
p.join()
# result
['hello world']
در مثال فوق، کلاس Pipe موجود در ماژول multiprocessing بعد از استفاده در برنامه، دو کانکتور ایجاد میکند که با یکی از آنها در تابع f سیگنال میفرستیم و با دیگری در پراسس اصلی پیغام را دریافت میکنیم.
Lock چیست و چه کاربردهایی دارد؟
از Lock برای مدیریت منابع مشترک در برنامهنویسی Concurrent (همزمان) استفاده میشود؛ بدین صورت که با دو متد acquire برای اشغال کردن منبع و متد release برای آزاد کردن منبع استفاده میشود. به عنوان مثال داریم:
from multiprocessing import Process, Lock
def greeting(l, i):
l.acquire()
print 'hello', i
l.release()
if __name__ == '__main__':
lock = Lock()
names = ['Alex', 'sam', 'Bernard', 'Patrick', 'Jude', 'Williams']
for name in names:
Process(target=greeting, args=(lock, name)).start()
#result
hello Alex
hello sam
hello Bernard
hello Patrick
hello Jude
hello Williams
در کد فوق، هنگامی که از acquire استفاده میکنیم، کلیۀ پراسسهای دیگری که از آن فانکشن استفاده میکنند را بلاک (مسدود) کرده تا پراسس مورد نظر کارش را به اتمام برساند و بعد از اینکه متد release را صدا کرد، پراسسهای دیگر به فعالیت خود ادامه میدهند.