چند پردازشی در برنامه چیست؟
در معماری چند پردازشی، به جای استفاده از یک 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!
تعریف پردازش با ورودی
اگر تابعی که می خواهیم به صورت پردازش جدید اجرا شود یک یا چند ورودی از ما بخواهد، میتوان هنگام تعریف پردازش، این ورودی ها را نیز مشخص کنیم.
برای این کار در هنگام فراخوانی 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=(۱۰,)) 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 () داریم که یک رشته را به عنوان تنها ورودی خود دریافت کرده و آنرا به تعداد ۶ بار در خروجی چاپ می کند.
میخواهیم این تابع را با دو مقدار ورودی متفاوت و در دو پردازش مجزا اجرا کرده و نتیجه آنرا ببینیم.
کد برنامه ما برای چنین مسئله ای به صورت زیر خواهد بود.
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ |
from multiprocessing import Process def test2(x): for i in range(۶): 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
همانطور که میبینید دو پردازش ما در لابه لای هم به صورت همزمان در حال اجرا هستند.
البته توجه داشته باشید که به دلیل ساده بودن و سریع بودن برنامه، ممکن است اجرای اولین پردازش زمان زیادی به طور نیانجامد.
بنابراین ممکن است ابتدا تمام ۶ مورد p1 و سپس شش مورد p2 چاپ شوند.
اما باز هم خللی در ساختار و فرآیندی که گفته شد به وجود نخواهد آمد.
جمع بندی برنامه نویسی چند پردازشی در پایتون
در این مقاله سعی شد به صورت کاملاً ساده و ابتدایی مبحث چند پردازشی در پایتون و نحوه پیاده سازی آن با یک مثال ساده را بررسی کنیم. چند پردازشی جزئیات زیادی دارد؛ برای مثال در صورتی که بخواهیم پردازش های ایجاد شده ما به یک داده مشترک دسترسی داشته باشند.