سلام! توی این قسمت از یکبار برای همیشه ادامهی قسمت قبلی در رابطه با موازیسازی در پایتون رو در پیش داریم. در یکبار برای همیشه بحثی رو مطرح میکنیم که خیلی در نگاه اول شاید راحت بنظر نرسه، و اینجا سعی میکنیم با بیان سادهتر و مثالهای خیلی خیلی ابتدایی اون مطلب رو به راحتی به مخاطب انتقال بدیم.
هدف اصلی ما توی این قسمت معرفی یکی از راهحلهایی هست که به کمک اون میتونیم به آقای GIL غلبه کنیم.
در قسمت قبل در این رابطه صحبت کردیم که مفسر پایتون thread-safe نیست در نتیجه امکان حداکثر بهرهوری از منابع پردازشی سیستم توسط مالتیتردینگ برای ما میسر نیست. به این موضوع هم اشاره کردیم که این روند روی اجرای برنامه های I/O bound خیلی تاثیری منفی نداره و تاثیر اون بیشتر در برنامههای CPU bound هست که اجازه کار با تمام هستههای پرازندهمون از ما گرفته شده است.
اما خب برای مشکل فوق هم راه حلهای مختلفی معرفی شده که در این آموزش قصد بررسی یکی از این راهحل رو داریم. مساله اصلی اینجا بود که تردهای مختلف برای اجرای کدهای مربوط به خودش باید صبر کنند تا GIL را بدست بیارند (Acquire) و همین مساله در عمل مساوی هست با اجرای یک ترد در هر لحظه از زمان. این مشکل در یک پردازنده چند هستهای یعنی فقط یک هسته از هستههای پردازنده به کار گرفته میشه.
راهحلی که کمک میکنه تا بتونیم از دستان پر توانِ GIL فرار کنیم، استفاده از پراسس به جای ترد هست.
پراسس چطور میتونه این مشکل رو حل کنه؟ توی این حالت در حقیقت هر پراسس یک مفسر پایتونِ جداگانه داره و از طرفی هر پراسس رو روی یک هسته از پردازنده اجرا میشه یعنی هر پراسس GIL مربوط به خودش رو داره. درنتیجه هیچ پراسسی منتظر گرفتن GIL نمیمونه و اینجوری تا حد خیلی زیادی میتونیم از منبع پردازشی سیستم به صورت خیلی موثر استفاده کنیم.
در قسمت قبل گفتیم که مفسرِ پایتون در حالت single-thread به خاطر وجود GIL نسبت به زبانهای برنامهنویسی دیگه خیلی کارآمدتر عمل میکنه و همین دلیل باعث میشه تا توی حالت مالتیپراسس هم تا حد زیادی از تمام منابع پردازشی سیستم بهره ببریم.
خوشبختانه توسعهدهندههای پایتون یک ماژول تر و تمیز برای این کار نوشتند که اسم اون ماژول multiprocessing هست و این ماژول بشدت شبیه threading هست. تو آموزش قبلی یک مثالِCPU bound داشتیم که حدوداً ۶ ثانیه اجرای اون طول میکشید و سعی کردیم با اضافه کردن ترد بهترش کنیم اما تاثیرِ مثبتی نداشت! کد اولیهمون این شکلی بود:
برای تعریف یک پراسس توی پایتون کافیه از کلاس Process از ماژولِ multiprocessing کمک بگیریم.
همونطور که میبینید نحوه ساخت و متدهای اون بشدت شبیه ماژول threading هست. ساخت و کنترل پراسس برای ما تنبلها کار سختیه، پس بریم سراغ یک راهِ حل سادهتر!
پراسسپول، در واقع مجموعهای از پراسسها هستند که منتظر دریافت کار میمونند و جوابها به ما بر میگردونند. توی مثال پایین ما یک پراسسپول ساختیم و کارمون رو به پراسسها به کمک متدِ map وصل کردیم.
از اونجایی که قرار هست از تمام هستههای پرازندهمون استفاده کنیم، اولینگام این هست که بفهمیم پردازندهی ما چندهسته پردازشی داره. این کار رو میتونیم به کمک تابع cpu_count انجام بدیم.
دقت کنیم وقتی یک instance از Pool میسازیم خودش به صورت پیشفرض تعداد پراسسها رو به اندازه هستههای cpu میگیره (فقط به خاطر جنبهی آموزشی داخل پرانتز تعداد پراسسها رو مشخص کردیم).
با کمک ماژولِ multiprocessing، این زمان اجرا برنامه ۵۰درصد کاهش داشته و توی ۳ ثانیه اجرا شده، نشون میده که دوست خوبی میتونه توی این مسیر برای ما باشه.
تا این زمانی که من دارم این مطلب رو مینویسم، به کمک چهار روش(متد) میشه کارها رو به یک تردپول تقسیم کنیم. نکته دوم، این که تمام این متدها به صورت پیشفرض باعث بلاک شدن ادامهی اجرای برنامه میشن، یعنی بقیه کدهای پایین این متدها اجرا نمیشه تا این متدها کارشون تموم بشه یا به عبارت دیگه synchronously اجرا میشن. به همین خاطر تمام این متدها همشون یک نسخهی non-blocking هم دارند. برای اینکار کافیه به انتهای اسم این متدها async_ اضافه بشه تا به صورت asynchronously اجرا بشن به طور مثال map_async.
نکته سوم، اکثر این متدها فقط یک آرگومان برای ورودی تابع موردِ اجرا قبول میکنند که در حقیقت میتونیم یک لیست از tuple به عنوان ورودی بدیم تا داخل تابع مورد نظرمون اون tuple رو unpack کنیم.
خروجی:
خروجیاش با مثال قبلی هیچ تفاوتی نداره مهم memory efficient بودنش هست.
خروجی:
ماژول concurrent.futures
هم یک پراسسپول در اختیار ما میذاره اما چون رابط کاربری اون و همچنین کارایی اون سادهتر و کندتر از ماژول multiproessing بود، ترجیح من معرفی یکی از این دو تا ماژول بود.
به طور کلی میشه گفت مزیت اصلی این روش دور زدن GIL و به کارگیری تمام منابع پردازشی سیستم هست. اما خب حتما این روش هم معایبی داره! بزرگترین عیبی که میشه به این روش گرفت نبود یک حافظه مشترک بین پراسسها هست (Shared memory). توی قسمت قبل، پراسس با چندین ترد داشتیم، که همه در یک حافظه مشترک (توی پراسس اصلی) فعالیت میکردند. تصویر یک پراسس با چندین ترد با حافظه مشترک:
اما وقتی وارد دنیای مالتیپراسس میشیم، فضا کاملاً متفاوت میشه، اینجا پایتون باید برای هر پراسس یک کپی از فضای حافظه پراسس اصلی بسازه، تا این پراسس جدید بتونه فعالیت مورد نظر ما رو پردازش کنه، تصویر نحوه کارِ مالتیپراسس:
ارتباط بین پراسسها یا همون inter-process communication هم با استفاده از ماژولِ multiprocessing امکانپذیر هست که این توسط multiprocessing.Queue انجام میشه.
پس به طور کلی معایب این کار عبارتند از:
مشکل دیگهای که شاید بشه به multiprocessing گرفت، scalable نبودنش هست که ما رو مجبور میکنه به سمت چیزایی مثل Celery بریم. به عنوان جمعبندی بهترِ گفت که هر مسالهای راهحل خودش رو داره و ماژول multiprocessing توی خیلی از مواقع راهحل کمهزینه و مناسبی هست.
این بود پایان این قسمت از یکبار برای همیشه! اگر از این قسمت خوشتون اومده نظر خودتون رو در پایین با ما به اشتراک بذارید و این قسمت رو با دوستانِ پایتونی خودتون در شبکههای اجتماعی به اشتراک بذارید!
https://blog.sopticek.net/2017/06/03/concurrent-and-parallel-programming-in-python-part-2/
https://medium.com/apteo/multi-processing-in-python-ee0ce73a459b
https://docs.python.org/3/library/multiprocessing.html
https://github.com/GreatBahram/OnceForEver/tree/master/code_snippets/parallelism/part2-multiprocessing