چند پردازشی در پایتون

اجرای همزمان چندین پردازش در یک برنامه کمک بسیاری زیادی به سرعت اجرای برنامه و نیز سرعت عملکرد خواهد کرد.

در برنامه هایی که نیاز هست پردازش ها یا بخش هایی از کد به صورت همزمان اجرا شوند، از چند پردازشی یا Multi Processing استفاده میشود.

چند پردازشی در برنامه چیست ؟

در معماری چند پردازشی، به جای استفاده از یک CPU برای انجام محاسبات و کارها، از چندین CPU به صورت موازی و همزمان استفاده میکنیم.

پیاده سازی و اجرای سیستم های چند پردازشی، مسائل و بحث های گوناگونی دارد اما به طور خیلی کلی میتوان پیاده سازی یک سیستم چند پردازشی را در دو حالت زیر در نظر گرفت.

  • استفاده همزمان از چند CPU
  • استفاده از یک CPU با چند هسته پردازشی (core)

هر کدام از دو مورد فوق، مزایا و معایب خود را دارند که موضوع بحث ما در این مقاله نیست.

اکثر سیستم های جدیدی که امروزه از آنها استفاده میکنیم دارای یک CPU با چند هسته هستند و شما همواره در مورد تعداد هسته های CPU دستگاه های مختلف صحبت هایی را میشنوید.

به طور قطع استفاده از چندین پردازنده به صورت همزمان، میتواند سرعت پردازش ما را افزایش دهد. بنابراین بهتر است بتوانیم برنامه هایی که در مقیاس بزرگتری اجرا میشوند را به صورت همزمانی (parallel) اجرا کرد.

ایجاد پردازش جدید در پایتون

در ابتدا فرض میکنیم تابعی با نام test() داریم که یک متن ساده را برای ما چاپ میکند. در حالت عادی برای صدا زدن تابع به صورت زیر عمل خواهیم کرد.

def test():
    print("test function ran successfully!")

if __name__ == '__main__':
    test()

در صورتی که کد به صورت بالا اجرا شود، هم برنامه اصلی و هم تابع test() در یک پردازنده اجرا و پردازش میشوند.

اما میخواهیم تابع test() در پردازنده دیگری از سیستم اجرا و پردازش شود.

ایجاد process جدید

برای این کار در زبان برنامه نویسی پایتون از ماژول multiprocessing کلاس Process را به برنامه خود اضافه میکنیم.

from multiprocessing import Process

حال با صدا زدن Process یک شئ از این کلاس ایجاد میکنیم تا بتوانیم تابع مورد نظر را به عنوان یک پردازش جدید اجرا کنیم.

برای ایجاد شئ از نوع Process کافی است نام تابع مورد نظر را به عنوان آرگومان target در هنگام صدا زدن سازنده کلاس ارسال کنیم. یعنی چیزی مشابه زیر:

p = Process(target=test)

اجرای پردازش ایجاد شده

اکنون که یک شئ از نوع فرآیند (Process) داریم، با فراخوانی تابع start() بر روی آن میتوانیم فرآیند را اجرا کنیم.

p.start()

پس از این فراخوانی، تابع test() به صورت یک پردازش جدید و در پردازنده ای دیگر اجرا خواهد شد.

جلوگیری از پیشروی برنامه فعلی تا اتمام اجرای پردازش

پس از start شدن پردازش، برنامه فعلی به اجرای خودش ادامه خواهد داد. اگر بخواهیم برنامه فعلی تا اتمام پردازش اجرا شده منتظر بماند و ادامه آن اجرا نشود، میتوانیم از تابع join() استفاده کنیم.

این تابع بر روی شئ فرآیند صدا زده شده و باعث میشود اجرای برنامه فعلی تا اتمام پردازش مورد نظر متوقف شود.

کد نهایی اجرای پردازش جدید به صورت زیر خواهد شد:

from multiprocessing import Process

def test():
    print("test function ran successfully!")

if __name__ == '__main__':
    p = Process(target=test)
    p.start()
    p.join()

# result:
# test function ran successfully!
ماژول در پایتون : آموزش تعریف Module و استفاده با ۲ مثال

ماژول در پایتون : آموزش تعریف Module و استفاده با ۲ مثال

تعریف پردازش با ورودی

اگر تابعی که میخواهیم به صورت پردازش جدید اجرا شود یک یا چند ورودی از ما بخواهد، میتوان هنگام تعریف پردازش، این ورودی ها را نیز مشخص کنیم.

برای این کار در هنگام فراخوانی Process یک آرگومان args برای آن تعریف میکنیم.

p = Process( target=test, args=( arg1, arg2, ) )

یک نمونه ساده استفاده از ورودی args به صورت زیر می باشد.

from multiprocessing import Process

def test(x):
    print("test function ran successfully! and x = " + str(x))

if __name__ == '__main__':
    p = Process(target=test, args=(10,))
    p.start()
    p.join()

# result:
# test function ran successfully! and x = 10

پردازش جدید در پردازنده چگونه ایجاد میشود ؟

اگر سیستم ما چندین CPU مجزا از هم داشته باشد، به ازای هر CPU منابع مخصوصی خواهیم داشت و سیستم عامل و در برخی معماری یک پردازنده اصلی به نام Master وظیفه تخصیص وظایف را به هر پردازنده بر عهده دارند.

در صورتی که یک CPU با چند هسته پردازشی داشته باشیم، در اکثر موارد، برخی منابع میان تمام هسته ها اشتراکی خواهد بود، مثل گذرگاه داده (Data Bus).

همانطور که میدانید هر پردازش در سیستم عامل دارای یک id جهت شناسایی است. برای ببینیم آیا با ایجاد یک Process جدید، واقعا یک پردازش با pid جدید ایجاد شده یا نه، میتوانیم مقدار pid را در برنامه اصلی و نیز تابع test() مورد بررسی قرار دهیم.

برای به دست آوردن pid فرآیندی که در حال پردازش برنامه فعلی است میتوان از ماژول os کمک گرفت.

import os

در ماژول os تابعی با نام getpid() وجود دارد که مقدار pid پردازش فعلی را به ما باز میگرداند.

os.getpid()

با جایگذاری تابع فوق در بخش های مناسبی از کد، کدی مشابه زیر خواهیم داشت.

from multiprocessing import Process
import os

def test():
    print("process ID is: " + str( os.getpid() ))

if __name__ == '__main__':
    print("main process ID is: " + str( os.getpid() ))
    p = Process(target=test)
    p.start()
    p.join()

با اجرای این برنامه نتیجه ای مشابه زیر به دست خواهیم آورد. دقت داشته باشید که pid برنامه در هر سیستم و در هر زمانی متفاوت است.

# result:
# main process ID is: 12924
# process ID is: 14100

اجرای چند برنامه همزمان

مزیت استفاده از چند پردازشی در پایتون یا هر زبان دیگر (Multi Processing) در برنامه ها، اجرای چند فرآیند به صورت همزمان است.

ما میتوانیم در برنامه خود چندین پردازش ایجاد کرده و همه آنها را با یکدیگر اجرا کنیم و به پردازنده ها اجازه دهیم به صورت همزمان در کنار هم کار کنند.

برای آنکه بهتر متوجه اجرای همزمان برنامه ها شوید، یک مثال ساده و ابتدایی را در ادامه بررسی خواهیم کرد.

فرض فرض کنید تابعی به نام test2() داریم که یک رشته را به عنوان تنها ورودی خود دریافت کرده و آنرا به تعداد 6 بار در خروجی چاپ میکند.

میخواهیم این تابع را با دو مقدار ورودی متفاوت و در دو پردازش مجزا اجرا کرده و نتیجه آنرا ببینیم.

کد برنامه ما برای چنین مسئله ای به صورت زیر خواهد بود.

from multiprocessing import Process

def test2(x):
    for i in range(6):
        print(x)

if __name__ == '__main__':
    p1 = Process(target=test2, args=("p1",))
    p2 = Process(target=test2, args=("p2",))
    p1.start()
    p2.start()
    p1.join()
    p2.join()

با اجرای این برنامه یکی از خروجی های ممکن به صورت زیر میشود.

# result:
# p2
# p1
# p2
# p1
# p1
# p2
# p1
# p2
# p1
# p2
# p1
# p2

همانطور که میبینید دو پردازش ما در لابه لای هم به صورت همزمان در حال اجرا هستند.

البته توجه داشته باشید که به دلیل ساده بودن و سریع بودن برنامه، ممکن است اجرای اولین پردازش زمان زیادی به طور نیانجامد.

بنابراین ممکن است ابتدا تمام 6 مورد p1 و سپس شش مورد p2 چاپ شوند.

اما باز هم خللی در ساختار و فرآیندی که گفته شد به وجود نخواهد آمد.

جمع بندی برنامه نویسی چند پردازشی در پایتون

در این مقاله سعی شد به صورت کاملاً ساده و ابتدایی مبحث چند پردازشی در پایتون و نحوه پیاده سازی آن با یک مثال ساده را بررسی کنیم.

چند پردازشی جزئیات زیادی دارد؛ برای مثال در صورتی که بخواهیم پردازش های ایجاد شده ما به یک داده مشترک دسترسی داشته باشند.

در این صورت مشکلاتی نظیر همزمانی و تغییر داده مشترک وجود خواهد داشت که میتوان با روش های خاصی آنها را مدیریت کرد.