Multiprocessing
در برنامه های مدرن معمولا بیش از یک پردازش، به صورت موازی( parallel )، توسط هر برنامه مدیریت و اجرا می شوند. برنامه های Backend، هوش مصنوعی و مسیریابی نقشه برداری (SLAM) در هر لحظه از چند هسته از پردازنده استفاده میکنند و پردازش های خود را به صورت موازی(رشته های همزمان یا concurrent threads ) روی سخت افزار انجام میدهند. حتی رابط های کاربری نیز عملیات خود را در پس زمینه( in the background ) انجام میدهند و مدیریت می کنند و یک thread را برای پاسخگویی به user events یا requests آزاد باقی می گذارند. فرض کنید در عملیات برنامه ی ما 3 مرحله ی A و B و C رخ میدهند که به یکدیگر ارتباطی ندارند. با دانش فعلی خود و به صورت خطی و یک پردازش single thread ، روند اجرای کد ما به صورت زیر خواهد بود:
اما از آنجایی که این عملیات از یکدیگر مجزا هستند میتوانیم به صورت موازی و با Multithreaded processing به صورت زیر آنها را اجرا کنیم:
برای مراحل A و B و C که به یکدیگر ارتباطی ندارند، روش Multithreaded processing بسیار سریع تر خواهد بود اگر که تعداد پردازنده های لازم و کافی را داشته باشیم.
خود Python بعضی از کارهای داخلی خود را به صورت خودکار با Multithreaded processing پیش میبرد؛ این مسئله باعث ایجاد محدودیت هایی برای نوشتن کد های Multithreaded processing در زبان پایتون می شود اما راه های امنی برای نوشتن کدهای Multithreaded processing در کنار این محدودیت ها وجود دارد.
نکته:
محدودیت اصلی GIL است که پیشنهاد میکنیم در مورد آن مطالعه کنید. GIL قرار است که در نسخه ی 3.13 و 3.14 پایتون دچار تغییراتی شود و بسیاری از این محدودیت ها رفع خواهند شد و پایتون تا انتهای سال 2024 به یک زبان مناسب برای Multithreaded processing تبدیل می شود.
https://wiki.python.org/moin/GlobalInterpreterLock
https://docs.python.org/3.13/whatsnew/3.13.html
راه های امن:
- استفاده از کتابخانه های داخلی مثل multiprocessing و خارجی مثل execnet
- استفاده از دو مفسر مجزا(روی یک سیستم یا با استفاده از Docker) به عنوان دو هسته که یک برنامه را اجرا می کنند.
- استفاده از یک thread جدید روی یک مفسر فعال با استفاده از کتابخانه ی داخلی threading
اولین روش به علت محدودیت های پایتون بهترین روش و ساده ترین روش است. روش دوم نیز روش خوبی است اما مصرف منابع بیشتری را می طلبد. روش سوم کمی پیچیده است، سخت تر به حساب می آید و منابع زیادی توسط Global Interpreter Lock (GIL) به کار گرفته می شوند و در هر لحظه تنها یک thread امکان استفاده از اجرای دستورات خود را دارد.
برای انتخاب روش درست بهتر است از اول به اولین روش فکر کنید و سپس به دومین و تا قبل از استفاده از پایتون 3.14 به روش سوم فکر نکنید!
تجربه:
در اولین موازی سازی یک Simultaneous localization and mapping (SLAM) به سراغ کتابخانه ی داخلی threading رفتم و برنامه ی من 2 بخش localization و mapping داشت و مرتبا میدیدم که از آنجایی که پردازش mapping سنگین تر و بیشتر بود بخش localization به مدت چند دقیقه اجرا نمی شد و برای رباتی که باید مرتبا در حال حرکت می بود این اتفاق بسیار احمقانه و نادرست بود. بعد از آن با انتقال کد از threading به multiprocessing برنامه 10درصد کندتر عمل می کرد، اما دیگر خبری از هیچ توقفی نبود و همه چیز به درستی کار می کرد. فکر میکردم چون برنامه نویس خوبی هستم میتوانم به درستی از threading استفاده کنم ولی محدودیت های GIL بیشتر از توان من برای پیاده سازی درست برنامه بود. در آخر من از threading برای مدیریت کارهای حساس درون پردازش های ایجاد شده با multiprocessing استفاده میکردم.
البته اگر قرار است با اشتراک گذاری حافظه بین concurrent processes سر و کار داشته باشید یا با ورودی و خروجی های سطح پایین(ttl و I/O ) سرو کار دارید باید از روش سوم استفاده کنید.