درآمدی بر مبحث Multi Processing در پایتون

درآمدی بر مبحث Multi Processing در پایتون

در این مقاله قصد داریم به تفصیل دربارۀ مبحث 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 را صدا کرد، پراسس‌های دیگر به فعالیت خود ادامه می‌دهند.

منبع


محمد طاهری