فکر میکنید پایتون میتونه دو تا کار رو همزمان انجام بده؟

تو این پست میخوام درباره برنامه نویسی موازی صحبت کنم اما قبلش باید چند تا اصطلاح رو تعریف کنم.

 

انواع فرآیندها

فرآیندها دو نوع هستند Sync و Async

Sync

یک فرآیند sync وابسته به یک فرآیند دیگه هست یعنی اینکه باید با اون فرآیند sync یا هماهنگ باشه و باید منتظر اتمام اون فرآیند باشه بعد کار خودش رو شروع کنه برای مثال فرآیند ذخیره پست در همین سیستم وردپرس رو در نظر بگیرید در این فرآیند برنامه برای ذخیره پست، اون  رو به دیتابیس میفرسته و حالا باید منتظر بمونه تا فرآیند دیتابیس پیام موفقیت یا عدم موفقیت رو برگردونه تا کارش رو ادامه بده بعد پیام نهایی رو به کاربر نشون بده پس این دو فرآیند باید با هم sync باشن.

Async

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

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

Sync یا Async بودن یک فرآیند به ماهیت و ساختار اون فرآیند و عملیاتی که انجام میده بستگی داره نه چیز دیگه ای

حالا اگه فرآیند های شما قابلیت اجرای مستقل از هم رو داشته باشه یعنی async باشن این قابلیت رو دارن که به صورت همزمان یا concurrent  اجرا بشه.

 

Concurrency

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

Parallelism

موازی سازی مرحله بعدی هست یعنی فرآیندهایی که به صورت concurrent ساختار دهی شدن حالا به صورت موازی و همزمان اجرا بشن

 

پس Concurreny مربوط به ساختار برنامه هست و Parallelism مربوط به اجرا

 

برای مثال در نظر بگیرید که تعداد اتصال های فعال یک دیتابیس فقط یه دونه میتون باشه حالا فرض کنید فرآیندی هست که به دیتابیس وصل میشه و مقداری رو میخونه.

حالا اگه ما طوری برنامه رو طوری ساختار دهی کنیم که ۴ تا از این فرآیندها به صورت مستقل بتونن اجرا بشن ما این برنامه رو به صورت Concurrent در اوردیم ولی در هر لحظه به خاطر محدودیت (برای مقال) سخت افزاری که هست فقط یک فرآیند میتونه به دیتابیس وصل بشه یعنی در هر لحظه عملا یک فرایند میتونه کارش رو انجام بده حالا اگه ما تعداد اتصال های مجاز رو ۳ تا کنیم (به وسیله ارتقا سخت افزار) در هر لحظه ۳ تا فرآیند میتونن اجرا شن که این میشه Parallelism.

یک مثال دیگه، فرآیند دریافت بلیط رو در نظر بگیرید (هیچ محدودیتی در تعداد بلیط موجود، وجود نداره) اگه یک باجه بلیط فروشی داشته باشیم و برای دریافت بلیط از باجه ۴ صف تشکیل بدیم این سیستم رو به صورت Concurrent ساختار دهی کردیم ولی در هر لحظه فقط یک نفر میتونه بلیط بخره.

اگه تعداد باجه ها رو بیشتر کنیم و ۳ تاش کنیم حالا به صورت همزمان بلیط فروشی داره انجام میشه و در هر لحظه به ۳ نفر به صورت همزمان بلیط فروخته میشه.

 

برنامه نویسی موازی در پایتون

برای برنامه نویسی موازی و پردازش همزمان در پایتون چند تا ماژول مطرح داریم

  1. multithreading
  2. multiprocessing
  3. concurrent.futures
  4. gevent
  5. asyncio

بزارید thread و process رو تعریف کنم

thread یک سری دستورالعمل هست که توسط یک process اجرا میشه و وابسته به اون هم خواهد بود و ایجادش خیلی کم هزینه هست.

process برخلاف thread کاملا مستقل هست و ایجادش هزینه ی زیادی داره

 

ماژول multithreading از thread و ماژول multiprocessing از process برای اجرای تسکها استفاده میکنه.

 concurrent.futures نسخه جدید و ادغام شده دو ماژول mutithreading و multiprocessing در نسخه ۳ پایتون هست، اینجا میتونید آموزشش رو بخونید.

gevent از یک نوع خاص ترد به نام Green Thread استفاده میکنه. این ماژول برای تسک هایی که نیازمند IO هستند بسیار کارامد هست.

asyncio هم از یک مدل خاص برای اجرا چند تسک استفاده میکنه که در پایتون ۳٫۶ معرفی شد.

اینجا فقط با ۲ تا ماژول اول کار داریم.

 

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

 

چرا با ترد نمیتونیم به parallelism واقعی برسیم؟ دلیلش وجود GIL یا Global Interpreter Lock هست.

GIL چیست؟

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

این مکانیزم برای این ایجاد شده تا دو تا ترد به یک قسمت مشخص از حافظه به صورت همزمان دسترسی نداشته باشن پس در نتیجه برنامه درست کار خواهد کرد. بقیه زبان های برنامه نویسی مثل C++, Java و Golang این مکانیزم رو ندارن و از یک روش دیگه برای حل مشکل دسترسی همزمان به حافظه استفاده کردن.

دلیل اینکه پایتون از GIL استفاده کرده و نه از روش دیگه ای هم این هست که پایتون یک زبان قدیمی هست و در اون موقع ها تمام CPU ها تک هسته ای بودن.

حالا میدونید این تسک ها به چه نوبتی اجرا میشن؟ در واقع مدل اجرای اونها رو میدونید؟

 

انواع مدل مدیریت چندوظیفگی

 

preemptive multitasking

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

cooperative multitasking

در این مدل، تسک ها به صورت داوطلبانه کنترل رو به همدیگه پاس میدن و با همکاری هم تمام تسک ها اجرا میشه. این واگذاری در دو حالت اتفاق می افته: زمانی که منتظر IO باشن و یا به صورت دوره ای. ماژول asyncio از این مدل برای اجرا تسک ها استفاده میکنه. asyncio به صورت پیش فرض تنها با استفاده از یک ترد تمام تسک ها رو اجرا میکنه.

 

همونطور که گفتم پایتون برای مدیریت و اجرای ترد ها از preemptive multitasking  استفاده میکنه، در این مدل کنترل ترد ها دست سیستم عامل هست اگه شما با ۱۰ تا ترد برنامتون رو اجرا کنید ابتدا ترد ۱ اجرا میشه بعد از اینکه یک مقدار مشخصی اجرا شد - در پایتون ۲ یعنی وقتی که به اندازه ۱۰۰ بایت کد پایتون اجرا بشه و در پایتون ۳ وقتی که ۱۰ میلی ثانیه بگذره - یا اینکه اون ترد منتظر یک عملیات IO بشه، سیستم عامل کنترل رو از اون ترد میگیره، حالا کنترل دست سیستم عامل هست و اون تصمیم میگیره که کنترل به همون ترد برگرده یا یه ترد دیگه شروع بشه و به همین ترتیب ترد ها رو اجرا میکنه تا کارشون تموم بشه. به این صورت تسک ها نه به صورت ترتیبی اجرا میشن و نه به صورت همزمان بلکه بین این دو حالت هست و شما احساس میکنید که تسک ها به صورت همزمان داره اجرا میشه.

برای اینکه تصمیم بگیرید که از thread استفاده کنید یا process یک قانون کلی هست که میگه اگه اون فرآیند نیازمند IO باشه از thread استفاده کنید و اگه محاسباتی باشه و نیازمند CPU از process استفاده کنید.

 

مقایسه عملکرد ترد و پروسس

همونطور که گفتیم اگه تسک شما IO باشه مثل دریافت چند صفحه وب بهتر هست از ترد استفاده کنید ولی اگه تسکتون محاسباتی هست بهتره از پروسس استفاده کنید.

تابع فیبوناچی که یک عملیات محاسباتی هست رو در نظر بگیرید

اگه این کد رو اجرا کنم

نتیجه

پس اگه دو بار مقدار فیبوناچی عدد ۳۳ رو به صورت ترتیبی محاسبه کنیم حدود ۶ ثانیه زمان میبره.

حالا همین کار رو با ۲ تا ترد انجام میدیم.

نتیجه

همونطور که دیدید اجرای تابع با ترد هیچ فرقی با اجرای ترتیبی کد نداشت.

حالا بزارید کد بالا رو با ۲ تا پروسس اجرا کنیم.

نتیجه

تقریبا همون ۳ ثانیه یعنی ۲ تا تسک همزمان اجرا شد.

حالا یک عملیات IO رو امتحان میکنیم.

تو این تست میخوایم ۳۰ صفحه وب رو با ۱۰ تا ترد و ۱۰ تا پروسس بگیریم.

نسخه ترتیبی این برنامه حدود چند دقیقه طول میکشه

نسخه ترد

زمان

نسخه پروسس

زمان

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

 

نتیجه

  • parallelism واقعی در پایتون تنها با استفاده از پروسس به دست میاد.
  • برای تسک های نیازمند IO از ترد و برای تسک های نیازمند CPU از پروسس استفاه کنید.
  • اگر از ترد استفاده کنید پایتون تنها از یک هسته cpu استفاده خواهد کرد.
  • اگه بخواید تعداد خیلی زیاد (چند ده هزار) تسک رو همزمان اجرا کنید (و زمان هم براتون مهم باشه) پایتون راه حل مناسبی برای شما نخواهد بود.
  • اگه تعداد تسک هاتون زیاد باشه شاید بخواید تعداد پروسس ها رو زیاد کنید ولی همونطور که گفتیم هزینه این کار (مثل نیاز به حافظه و ...) بسیار زیاد خواهد بود برای مثال مصرف حافظه خیلی زیاد خواهد شد در نتیجه عملا این کار غیر ممکن خواهد بود برای تعداد بالای تسک

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

۲ comments

  1. من خودم هم از ماژول‌های Process و JoinableQueue متعلق به multiprocessing پایتون استفاده میکنم و خیلی هم عالی کار میکند.. به اینصورت که یکی از توابع کلاس را به عنوان یک پروسس جداگانه تعریف کرده‌‌ام که کارش این است که یک آبجکتی را از صف تحویل گرفته، پردازش‌های مورد نظر را روی آن انجام داده، و آبجکت دریافت شده از صف را به عنوان انجام شده علامتگذاری میکند که جناب JoinableQueue مطلع شده و آبجکت بعدی را تحویل دهد:)
    در آخر هم در main صف را join کردم تا منتظر اتمام تمام پردازش ها بماند.

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

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *