در این بلاگ از ویراوب123 میخواهیم در مورد برنامه نویسی همروندی و موازی در پایتون صحبت کنیم.
آیا در برنامه نویسی همروندی هم مانند برنامه نویسی موازی هر دو تسک به صورت همزمان انجام میشه؟
هر دو روش هدفشون اینه که چندین کار را در یک زمان خاص به صورت همزمان اجرا کنند.
برنامه نویسی همروندی Concurrent programming
در برنامه نویسی همروندی با سرعت خیلی زیاد بین چندین کار سوییچ میکنیم. چون خیلی سریع سوییچ میکنیم وقتی یک نفر از بیرون نگاه میکنه فکر میکنه داریم چند تا کار را همزمان انجام میدیم.
ولی اگر به صورت جزئی به عملکرد سخت افزار نگاه کنیم میبینم چند تا کار همزمان انجام نمیشه بلکه به صورت خطی است و در هر لحظه از زمان یک task انجام میشه.
برنامه نویسی موازی Parallel programming
در برنامه نویسی موازی دو task در یک لحظه از زمان با هم اجرا میشوند.
مثل این میمونه که یک task را بدیم به کامپیوتر یک و task دیگر را بدیم به کامپیوتر دو. اینجوری هر دو task به صورت همزمان با هم اجرا میشوند.
در دنیای امروز cpu ها از چندین هسته تشکیل شدند. این هسته ها در واقع هر کدامشان یک پردازشگر جدا هستند. و میتوانیم هر task را به یک هسته بدیم تا انجام بده و برنامه نویسی موازی را به کمک هسته های یک CPU اجرا کنیم.
مفهوم thread در سیستم عامل
منظور از thread یا نخ در سیستم عامل چیه؟
مثلا وقتی میگن سیستم عامل داره از چندتا thread استفاده میکنه منظورشون چیه؟
آیا برنامه نویس میتونه نحوه استفاده از thread ها را مدیریت یا تغییر بده؟
وقتی یک برنامه را در سیستم عامل اجرا میکنیم تبدیل میشه به process و سیستم عامل یک عدد منحصر به فردی را به نام PID به آن process اختصاص میدهد .
هر process از چندین واحد اجرایی به نام thread ساخته شده است که در واقع این thread ها هستند که process را اجرا میکنند.
وقتی یک process در سیستم عامل ساخته میشه به صورت پیش فرض یک thread به نام main thread دارد که واحد اصلی اجرایی پروسه است و پروسه با این thread شروع میشه . برنامه نویس میتونه به دلخواه به پروسه thread های دیگه ای را اضافه کنه.
مثلا اگر یک فایل پایتون ۸ خطی داشته باشیم به صورت پیش فرض یک thread هشت خط را اجرا میکنه ولی برنامه نویس میتونه جوری طراحی کنه که ۴ خط اول را thread1 و چهار خط دوم را thread2 اجرا کنه.
مزیت این روش اینه که میتونیم با کمک thread ها در یک بازه زمانی چندتا کار را با هم انجام بدیم. برای این کار میتونیم از ایده همروندی و موازی استفاده کنیم.
نحوه کار سیستم عامل در حالت multithreading
وقتی که چندتا thread یا multithreading داریم این thread ها به روش همروندی یا روش موازی پروسه را انجام میدهند ؟
به سخت افزار و سیستم عامل بستگی دارد. خود سیستم عامل thread ها را مدیریت میکند.
به طور کلی در دنیای واقعی به صورت ترکیبی از همروندی و موازی پروسه تکمیل میشه.
مثلا فرض کنید شرایط زیر را داریم:
سیستم امون ۸ تا thread دارد که این thread ها میتوانند مربوط به processهای متفاوتی باشند.
یک CPU چهار هسته ای داریم.همانطور که میدونید هر هسته از cpu خودش یک پردازشگر جدا است و هسته ها میتوانند به صورت موازی با هم کارها را انجام بدهند.
با شرایط بالا سیستم عامل میتونه به صورت ترکیبی thread ها را اجرا کنه. مثلا سیستم عامل ۴ تا thread برمیداره و به صورت موازی روی ۴ هسته اجرا میکنه. فرض کنید مطابق شکل در مرحله اول thread های 1-3-5-7 روی ۴ هسته به صورت موازی اجرا میشوند.
در حال حاضر چهار thread با شماره های 2-4-6-8 باقی میمونند این ۴ thread را دوباره به صورت موازی روی ۴ هسته اجرا میکنه و این کار را تکرار میکند.
اگر دقت کنید هر هسته به صورت همروندی دو تا thread را انجام میدهد. مثلا مطابق شکل، هسته یک thread شماره یک و سه را به صورت همروندی اجرا میکند. این در حالی است که thread های شماره 1-3-5-7 به صورت موازی اجرا میشوند.
منتقدین پایتون میگن که پایتون کند است آیا این درسته؟
چرا پایتون اجازه اجرای موازی بین thread ها را نمیده؟ سرعت اجرا در حالت موازی خیلی بهتر از همروندی است. چرا در پایتون برنامه نویسی موازی را نداریم و باعث شده سرعت اجراش کمتر بشه؟
یک برنامه پایتونی که مینویسیم در سیستم عامل تبدیل به یک پروسه میشه که میتونیم براش چندین thread بسازیم.
پایتون به گونه ای نوشته شده که اجازه نمیده دو تا thread از یک برنامه به صورت موازی با هم اجرا بشوند. مثلا طبق شکل بالا امکان ندارد thread شماره یک و سه از یک برنامه پایتونی باشند.
پایتون تنها اجازه میده که thread ها به صورت همروندی اجرا بشوند.
برنامه نویسی موازی رو سرعت خیلی تاثیر داره و میشه گفت از نظر تئوری سرعت را دو برابر میکنه
در حالی که برای اجرا به صورت همروندی چون دائما بین threadها سوییچ میکنیم سرعت کمتر، حتی ممکنه اگر با یه thread برنامه را اجرا کنیم سرعت بهتر از حالت همروندی باشه چون یه زمانی را صرف سوییچ کردن میکنیم.
چیزی که باعث میشه پایتون امکان موازی اجرا شدن چندین thread را نده به خاطر وجود چیزی به اسم global interpreter lock یا GIL است.
در نسخه استاندارد پایتون که به زبان c نوشته شده و به آن cpython گفته میشه GIL وجود دارد.
در واقع GIL مثل یک قفل میمونه که اجازه نمیده دو یا چند تا thread به صورت موازی با هم اجرا بشوند. در هر نقطه از زمان اجرا، قفل GIL مطمئن میشه که فقط یک thread در حال اجرا است و هیچ اجرای موازی وجود ندارد.
علت استفاده از global interpreter lock در پایتون
چرا سازنده های پایتون از GIL استفاده کردند. و باعث شدن اجازه اجرای موازی thread ها را نداشته باشیم؟
به خاطر پیچیدگی مدیریت حافظه.
وقتی دو تا thread به صورت موازی اجرا بشوند سرعت اجرا دو برابر میشه ولی بهاش اینه که هماهنگ کردن بین thread ها سخت و پیچیده میشه. و thread ها تو حالت موازی باید با هم هماهنگ باشند وگرنه مشکل ساز میشه.
مشکل دوم اینه که حافظه thread ها در حالت موازی به اشتراک گذاشته میشه. یعنی حافظه آن ها مشترک است.
فرض کنید یک پروسه با آیدی 2400 داریم این پروسه یک حافظه و دو تا thread دارد .حافظه این پروسه بین دو thread مشترک است.
دو تا thread میتوانند به حافظه همدیگر دسترسی داشته باشند . البته به جز حافظه local توابع یا همان stack.
شما هر متغیری که در حافظه برنامه اتون تعریف میکنید بین تمام thread های برنامتون میتونه استفاده بشه و بهش دسترسی پیدا کنید به غیر از چیزایی که داخل scope توابع تعریف میکنید (مانند متغیر b در شکل)
مثلا اگر در thread1 تابعی به اسم name تعریف کردید و درون تابع یک متغیر به نام b تعریف کنید متغیر b فقط در thread1 و فقط درون تابع name قابل دسترسی است.
ولی اگر متغیر a را در thread1 بیرون از تابع تعریف کنید متغیر a در thread2 هم قابل دسترسی است. چون حافظه بین دو thread مشترک است.
حالا مشکل پیچیدگی در حالت موازی کجاست؟
فرض کنید در پروسه یا برنامه امون متغیر a=10 را خارج از توابع تعریف کردیم. پروسمون دو تا thread داره. الان هر دو thread به a=10 دسترسی دارند.
مثلا thread1 برنامه ریزی کرده که به متغیر a پنج تا اضافه کنه و thread2 برنامه ریزی کرده که متغیر a را برابر ۱۰۰ قرار بده.
اگر بذاریم دو تا thread به صورت موازی اجرا بشه. ترتیب اجرا threadها میتونه نتیجه a را متفاوت کند.
اگر اول thread1 اجرا بشه بعد thread2 :
Thread1 : a = 10 + 5 = 15
Thread2: a = 100
Result : a = 100
اگر اول thread2 اجرا بشه بعد thread1:
Thread2 : a = 100
Thread1 : a = 100 + 5 = 105
Result : a = 105
خب الان واقعا مقدار متغیر a چند؟
تو حالت عادی نمیتونیم مطمئن باشیم کدوم thread اول اجرا میشه. فرض کنید دو تا core داریم. Thread1 را روی core1 اجرا میکنیم و thread2 را روی core2 اجرا میکنیم. بستگی داره کدوم یکی از core ها سریعتر باشن و زودتر به متغیر a برسند و متغیر a را تغییر بدن. یا بستگی داره متغیر a تو کدوم فایل ها مقدارش زودتر تغییر کنه.
پس باید یه جوری دو برنامه از دو thread با هم هماهنگ و sync بشه. مثلا اگر میخواهید متغیر a اول تو thread2 مقدار بگیرد باید یه مکانیزم هماهنگ سازی داشته باشی که thread1 تا وقتی که thread2 متغیر a را عوض نکرده تغییری روی متغیر a اعمال نکنه.
پس نتیجه میگیریم اگر threadها موازی اجرا بشوند باید یه سری مکانیزم برای هماهنگ سازی بینشون وجود داشته باشه. حالا پایتون برای ساده سازی مدیریت حافظه گفته اصلا اجازه نمیدیم دو تا thread به صورت موازی اجرا بشوند.
اجرای threadها به صورت همروندی هم نیاز به هماهنگی دارد ولی کمتر از حالتی که به صورت موازی است.
پایتون تو حالت multithreading و برنامه نویسی همروندی چجوری تعیین میکنه از هر thread چقدر را انجام بده و دوباره سوییچ کنه؟
این مبحث خیلی گسترده است. اما به صورت خلاصه بگیم. به خاطر فعالیت های ورودی و خروجی ممکنه یه وقتایی بعضی از thread ها بیکار بشوند.
فرض کنید thread1 وظیفه داره از کاربر input بگیره. خب الان thread1 منتظر میمونه تا کاربر چیزی را وارد کنه. و برنامه عملا در زمانی که کاربر داره چیزی را وارد میکنه کاری انجام نمیده. خب پایتون به جای اینکه منتظر بمونه تا کاربر اطلاعاتش را وارد کنه میتونه سوییچ کنه رو thread2 و مقداری از کارش را انجام بده.
پس بر اساس زمان انتظار فعالیت های ورودی و خروجی ها میتونه تصمیم بگیره بین threadها سوییچ کنه.
اگر threadها همشون کارشون پردازشی باشه و فعالیت ورودی خروجی نداشته باشند چی؟
بعد از انجام دادن یه تعداد استاندارد کار که thread1 انجام داد پایتون خودش میاد سوییچ میکنه به thread2.
چرا از multithreading در پایتون استفاده کنیم؟
همانطور که میدونید امکان اجرای موازی بین threadها وجود نداره؟ پس چرا تو پایتون از multithreading استفاده میکنند؟ چه مزیتی ایجاد میکنه؟
ما دو نوع برنامه از نظر مصرف منابع داریم. برنامه های I/o bound و برنامه های cpu bound.
برنامه های I/o bound
برنامه هایی هستند که سرعت اجراشون به مدت زمانی که درگیر فعالیت ورودی خروجی هستند بستگی دارد. به طور کلی این برنامه ها اکثر زمانشون را صرف فعالیت ورودی و خروجی میکنند. مثلا برنامه downloader که وظیفه اش دانلود کردن یک فایل از اینترنت است یا برنامه ای که input از کاربر میگیره هر دو اکثر زمانشون را صرف فعالیت ورودی خروجی میکنند و جز دسته برنامه های I/o bound قرار میگیرند.
برنامه های cpu bound
این برنامه ها اکثر زمانشون را صرف پردازش با cpu میکنند. مثلا برنامه ای که. ب.م.م دو عدد خیلی بزرگ را حساب میکنه.
خب multithreading پایتون قطعا مناسب برنامه های cpu bound ها نیست. چرا؟
ما زمانی میتونیم سرعت یک برنامه cpu bound را زیاد کنیم که بتوانیم از هسته های cpu استفاده بیشتر و بهتری کنیم . پردازش موازی میتونه عملکرد برنامه های cpu bound را بیشتر کنه و سرعت کار را بالاتر ببره.
از آنجایی که پایتون اجازه پردازش موازی را نمیده پس مناسب برنامه های cpu bound نیست.
ولی برنامه پایتون برای برنامه های I/o bound مناسب است. مثلا فرض کنید یه برنامه دارید که میخواهید همزمان یک داده از کاربر بگیره و یک داده ای را از یک socket بخوانه.
بدون multithreading
بخش input گرفتن اجرا میشه و برنامه متوقف میشه تا کاربر چیزی را وارد کنه . بعد که وارد کرد میره سراغ خواندن از socket.
با multithreading
دو تا thread میسازیم . Thread اول برای گرفتن input از کاربر و thread دوم برای خواندن داده از socket.
پایتون میتواند به صورت همروندی دو تا thread را اجرا کنه . از آنجایی که خیلی سریع بین thread ها سوییچ میکنه شما فکر میکنید دو تا کار به صورت همزمان اجرا میشه.
آیا پایتون برای برنامه های cpu bound راهکاری ندارد؟
راهکار بهینه کردن برنامه های cpu bound در پایتون multiprocessing است.
چندین process جدا بسازیم که برنامه را اجرا کنند. مثلا فرض کنید task1 و task2 را داریم که جفتشون cpu bound هستند. اگر هر دو تا task را تو یک فایل پایتون بنویسید و سعی کنید با multithreading برنامه را اجرا کنید چون پایتون اجازه پردازش موازی را نمیده در هر لحظه از زمان فقط یکی از task ها اجرا میشه. راهکار ساده اینه که با دو تا فایل پایتون برنامه را اجرا کنید.
در این حالت دو پروسه جدا با دو تا process id جدا داریم که هر پروسه یک GIL جدا دارند. و GIL های هر پروسه روی پروسه دیگری تاثیر نمیذارد. در واقع هر GIL باعث میشه thread های اون برنامه به صورت موازی اجرا نشه.
پس الان دو تا پروسه جدا یا همان دو تا فایل پایتون جدا میتونند به صورت موازی روی دو تا هسته جدا از cpu به صورت موازی اجرا بشوند.
پس مفهوم multiprocessing میگه چندین پروسه بساز که هر کدامشان بتوانند توسط سیستم عامل به صورت موازی اجرا بشوند. حالا اگر منابع کافی و هسته آزاد داشته باشه این پروسه ها را به صورت موازی اجرا میکنه و اگر نداشته باشه به صورت همروندی. اما در کل سیستم عامل تلاش میکنه به بهترین و سریعترین حالت ممکن پروسه ها را روی cpu اجرا کنه و نهایت استفاده را از cpu ببره.
اطلاعات کاملتر در این لینک قرار دارد.
برنامه نویسی موازی و همروندی در پایتون