خانه توسعهدهنده تکنولوژی بکاند پایتون Coroutine در پایتون
Coroutine در پایتون
Coroutineها پایهی برنامهنویسی asynchronous و سنگ بنای async i/o در پایتون هستند. اما قبل از اینکه به نحوهی استفاده و پیادهسازی Coroutine در پایتون بپردازیم. باید ماهیت این مفهوم را جدا از پایتون درک کنیم؛ کاری که قصد داریم در بخش اول از این سری مقالهها انجام دهیم.
Coroutine چیست؟
Coroutine براساس Wikipedia عبارت است از: «اجزای برنامههای رایانهای که با اجازه دادن به تعلیق و ازسرگیری اجرا، زیرروالها را برای چندوظیفگی غیرپیشگیرانه تعمیم میدهند.» این تعریف زیاد قابلدرک نیست؛ به شکل سادهتر میتوان Coroutineها را گونهای از توابع دانست که همزمانی را از طریق چندوظیفگی مشارکتی (cooperative multitasking) امکانپذیر میکنند. همزمانی و چندوظیفگی که پایههای این تعریفاند در ادامه به تفضیل بررسی میشوند.
در پایهایترین سطح، همزمانی بر اجرای چندین وظیفه (Task) تمرکز دارد. تقریباً در تمامی برنامهها بیش از یک Task برای اجرا وجود دارد. در این حالت نیاز است چندین وظیفه انجام شود. راحتترین راهحل این است که این Taskها بهترتیب اجرا شوند. یعنی زمانی که یک وظیفه به اتمام رسید، وظیفهی دیگر شروع شود.
اما در واقعیت، بسیاری از Taskها بلوکهای صرفاً از محاسبه نیستند. بلکه اغلب شامل زمانهای زیادی میشوند که منتظر چیز دیگری هستند؛ برای مثال خواندن از دیسک، network request و…
در چنین حالتی اگر از همان روش ابتدایی برای اجرای وظایف استفاده کنیم، زمان زیادی را با بیکاری تلف میکنیم. اما اگر بتوانیم در زمانهای بیکاریِ هر وظیفه، وظیفهی دیگری را اجرا کنیم، در نهایت برای انجام تمام وظایف زمان کمتری موردنیاز است. این هستهی اصلی و هدف همزمانی (Concurrency) است.
Mulit-Processing
در واضحترین رویکرد تنها کافی است چندین Worker (اجراکننده) داشته باشیم تا هر کدام از Workerها هر یک از Taskها را در زمان، پردازش کند. یکی از راههای پیادهسازی آن در پایتون استفاده از multi processing است که در آن هر Worker پردازهی خود را دارد؛ به این معنا که هر Worker، رانتایم Cpython، پشته، حافظه و bytecodeهای مخصوص به خود را دارد.
اما این رویکرد هزینههای خود را نیز به همراه دارد؛ اول و مهمتر از همه، حافظهی تکراری مورداستفاده برای هر اجراست و دومین هزینه این است که هرگونه ارتباط بین پردازهها باید با سریالایز کردن داده انجام شود که به هر Task کار اضافه تحمیل میکند. اما در جنبهی مثبت این رویکرد هر Worker قادر به پردازش وظایف بهصورت موازی است که به ما امکان بهرهوری بیشتری از چندهستگی CPU میدهد. این راهحل، موازیسازی (Parallelism) نامیده میشود که میتوان آن را برادر/خواهرِ جوانتر همزمانی دانست. بهطور کلی زمانی که بازده کلی با تعدادی Woker (و تعدادی Task بهازای هر Worker) در نظر گرفته شود، همچنان هر Worker و به همراه آن هر پردازه همچنان زمانی که Task منتظر منابع میماند، بیکار است.
Threading
در یک راه جایگزین دیگر میتوان از تعدادی ترد به ازای هر Worker استفاده کرد. به این معنی که بهجای اضافه کردن هر پردازه به ازای هر Worker، تنها کافیست Threadهای متفاوت ایجاد کنیم. در این حالت برای اضافه کردن Worker، پشتهی مخصوص به خود را به آن اضافه کنیم که نیاز به تکرار حافظه و byte code را کاهش میدهد. همچنین به خاطر استفاده از حافظهی مشترک، نیاز به سریالاز شدن داده برای ارتباط بین Workerها وجود ندارد.
برای آشنایی بیشتر با تردها میتوانید به دورهی آموزش پایتون پیشرفته کوئرا کالج مراجعه کنید.
اما این راه نیز هزینههای خود را دارد. با توجه به طراحی منحصربهفرد Cpython، (با توجه به GIL – Global Interpreter Lock) امکان اجرای چندین ترد در یک لحظه وجود ندارد. بنابراین سایر تردها باید تا زمان نوبت اجرایشان بیکار بمانند. در کنار این runtime مسئول زمانبندی تردهاست بدون اینکه بینشی نسبت به کاری که انجام میدهند داشته باشد. هر بار که یک ترد انتخاب میشود باید منتظر Context Switch (شامل ذخیره پشتهی در حال اجرا و بازیابی پشتهی انتخابشده) پرهزینه باشیم. این راهحل همچنین با عنوان Pre-emptive MultiTasking شناخته میشود که دسترسی منصفانه به CPU را تضمین میکند. اما احتمال زیادی برای تعویض تردها در زمان نامناسب وجود دارد که منجر به رفتار غیربهینه مانند تأخیر در شروع درخواستهای شبکه میشود و این میتواند بهطور چشمگیری مدت زمانی که یک Task ممکن است طول بکشد را افزایش دهد.
Coroutine
در نتیجه هم Mulit-Processing و هم Threading مزایا و معایب خود را دارند، اما هیچیک از آنها شروع به نزدیک شدن به شکل ایدئال همزمانی که قبلتر اشاره شد، نمیکنند. چیزی که ما نیاز داریم سیستمی است که در آن هر Task مراحل اصلی خود را انجام میدهد و فقط زمانی با تسک دیگر جابهجا میشود که منتظر منابع خارجی است. به بیان دیگر باید هر Task تکمیل شود یا بهطور مشخص کنترل را واگذار کند تا Task دیگری بتواند اجرا شود که مشخصاً نقطهی این واگذاری کنترل، زمانی است که Task منتظر منابع خارجی است.
این سیستم با عنوان چندوظیفگی مشارکتی (cooperative multitasking) شناخته میشود که یک رویکرد ساده و بدون هزینه برای همزمانی (Concurrency) است و باعث میشود استفادهی بهتری از منابع داشته باشیم و بتوانیم همهی Taskها را در یک ترد و پردازه اجرا کنیم.
تا به اینجا با مفهوم Coroutine آشنا شدیم، اما چیزی که هنوز به آن نپرداختیم پیادهسازی و استفاده از Coroutine در پایتون است که آن را در بخش دوم از این سری مقاله بررسی خواهیم کرد.