Coroutine در پایتون

1821
coroutine در پایتون

Coroutineها پایه‌ی برنامه‌نویسی asynchronous و سنگ بنای async i/o در پایتون هستند. اما قبل از اینکه به نحوه‌ی استفاده و پیاده‌سازی Coroutine در پایتون بپردازیم. باید ماهیت این مفهوم را جدا از پایتون درک کنیم؛ کاری که قصد داریم در بخش اول از این سری مقاله‌ها انجام دهیم.

Coroutine چیست؟

Coroutine براساس Wikipedia عبارت است از: «اجزای برنامه‌های رایانه‌ای که با اجازه دادن به تعلیق و ازسرگیری اجرا، زیرروال‌ها را برای چندوظیفگی غیرپیشگیرانه تعمیم می‌دهند.» این تعریف زیاد قابل‌درک نیست؛ به شکل ساده‌تر می‌توان Coroutine‌ها را گونه‌ای از توابع دانست که هم‌زمانی را از طریق چند‌وظیفگی مشارکتی (cooperative multitasking) امکان‌پذیر می‌کنند. هم‌زمانی و چند‌وظیفگی که پایه‌های این تعریف‌اند در ادامه به تفضیل بررسی می‌شوند.

در پایه‌ای‌ترین سطح، هم‌زمانی بر اجرای چندین وظیفه (Task) تمرکز دارد. تقریباً در تمامی برنامه‌ها بیش از یک Task برای اجرا وجود دارد. در این حالت نیاز است چندین وظیفه انجام شود. راحت‌ترین راه‌‌حل این است که این Taskها به‌ترتیب اجرا شوند. یعنی زمانی که یک وظیفه به اتمام رسید، وظیفه‌ی دیگر شروع شود.

اجرای چند Task به ترتیب
اجرای چند Task به ترتیب

اما در واقعیت، بسیاری از Taskها بلوک‌های صرفاً از محاسبه نیستند. بلکه اغلب شامل زمان‌های زیادی می‌شوند که منتظر چیز دیگری هستند؛ برای مثال خواندن از دیسک، network request و…

اجرای چند Task به ترتیب با در نظر گرفتن زمان بیکاری
اجرای چند Task به ترتیب با در نظر گرفتن زمان بیکاری (بخش‌های روشن‌تر نشان‌دهنده‌ی زمان بیکاری است)

در چنین حالتی اگر از همان روش ابتدایی برای اجرای وظایف استفاده کنیم، زمان زیادی را با بیکاری تلف می‌کنیم. اما اگر بتوانیم در زمان‌های بیکاریِ هر وظیفه، وظیفه‌ی دیگری را اجرا کنیم، در نهایت برای انجام تمام وظایف زمان کمتری مورد‌نیاز است. این هسته‌ی اصلی و هدف هم‌زمانی (Concurrency) است.

Co-routine
اجرای سایر Task‌ها در زمان بیکاری هر Task

Mulit-Processing

در واضح‌ترین رویکرد تنها کافی است چندین Worker (اجرا‌کننده) داشته باشیم تا هر کدام از Worker‌ها هر یک از Task‌ها را در زمان، پردازش کند. یکی از راه‌های پیاده‌سازی آن در پایتون استفاده از multi processing است که در آن هر Worker پردازه‌ی خود را دارد؛ به این معنا که هر Worker، ران‌تایم Cpython، پشته، حافظه و ‌‌bytecode‌های مخصوص به خود را دارد.

Mulit-Processing
اجرای Task‌ها به Multi-Processing

اما این رویکرد هزینه‌های خود را نیز به همراه دارد؛ اول و مهم‌تر از همه، حافظه‌ی تکراری مورد‌استفاده برای هر اجراست و دومین هزینه این است که هر‌گونه ارتباط بین پردازه‌ها باید با سریالایز کردن داده انجام شود که به هر Task کار اضافه تحمیل می‌کند. اما در جنبه‌ی مثبت این رویکرد هر Worker قادر به پردازش وظایف به‌صورت موازی است که به ما امکان بهره‌وری بیشتری از چند‌هستگی CPU می‌دهد. این راه‌حل، موازی‌سازی (Parallelism) نامیده می‌شود که می‌توان آن را برادر/خواهرِ جوان‌تر هم‌زمانی دانست. به‌طور کلی زمانی که بازده کلی با تعدادی Woker (و تعدادی Task به‌ازای هر Worker) در نظر گرفته شود، همچنان هر Worker و به همراه آن هر پردازه همچنان زمانی که Task منتظر منابع می‌ماند، بیکار است.

Threading

در یک راه جایگزین دیگر می‌توان از تعدادی ترد به ازای هر Worker استفاده کرد. به این معنی که به‌جای اضافه کردن هر پردازه به ازای هر Worker، تنها کافیست Threadهای متفاوت ایجاد کنیم. در این حالت برای اضافه کردن Worker، پشته‌ی مخصوص به خود را به آن اضافه کنیم که نیاز به تکرار حافظه و byte code را کاهش می‌دهد. همچنین به خاطر استفاده از حافظه‌ی مشترک، نیاز به سریالاز شدن داده برای ارتباط بین Worker‌ها وجود ندارد.

برای آشنایی بیشتر با تردها می‌توانید به دوره‌ی آموزش پایتون پیشرفته کوئرا کالج مراجعه کنید.

Threading
اجرای Task‌ها به Threading

اما این راه نیز هزینه‌های خود را دارد. با توجه به طراحی منحصر‌به‌فرد Cpython، (با توجه به GIL – Global Interpreter Lock) امکان اجرای چندین ترد در یک لحظه وجود ندارد. بنابراین سایر ترد‌ها باید تا زمان نوبت اجرایشان بیکار بمانند. در کنار این runtime مسئول زمان‌بندی تردهاست بدون اینکه بینشی نسبت به کاری که انجام می‌دهند داشته باشد. هر بار که یک ترد انتخاب می‌شود باید منتظر Context Switch (شامل ذخیره پشته‌ی در حال اجرا و بازیابی پشته‌ی انتخاب‌شده) پرهزینه باشیم. این راه‌حل همچنین با عنوان Pre-emptive MultiTasking شناخته می‌شود که دسترسی منصفانه به CPU را تضمین می‌کند. اما احتمال زیادی برای تعویض ترد‌ها در زمان نامناسب وجود دارد که منجر به رفتار غیربهینه مانند تأخیر در شروع درخواست‌های شبکه می‌شود و این می‌تواند به‌طور چشمگیری مدت زمانی که یک Task ممکن است طول بکشد را افزایش دهد.

Coroutine

در نتیجه هم Mulit-Processing و هم Threading مزایا و معایب خود را دارند، اما هیچ‌یک از آن‌ها شروع به نزدیک شدن به شکل ایدئال هم‌زمانی که قبل‌تر اشاره شد، نمی‌کنند. چیزی که ما نیاز داریم سیستمی است که در آن هر Task مراحل اصلی خود را انجام می‌دهد و فقط زمانی با تسک دیگر جابه‌جا می‌شود که منتظر منابع خارجی است. به بیان دیگر باید هر Task تکمیل شود یا به‌طور مشخص کنترل را واگذار کند تا Task دیگری بتواند اجرا شود که مشخصاً نقطه‌ی این واگذاری کنترل، زمانی است که Task منتظر منابع خارجی است.

Co-routine
واگذاری کنترل با اتمام یا بیکاری Task

این سیستم با عنوان چند‌وظیفگی مشارکتی (cooperative multitasking) شناخته می‌شود که یک رویکرد ساده و بدون هزینه برای هم‌زمانی (Concurrency) است و باعث می‌شود استفاده‌ی بهتری از منابع داشته باشیم و بتوانیم همه‌ی Task‌ها را در یک ترد و پردازه اجرا کنیم.

تا به اینجا با مفهوم Coroutine آشنا شدیم، اما چیزی که هنوز به آن نپرداختیم پیاده‌سازی و استفاده از Coroutine در پایتون است که آن را در بخش دوم از این سری مقاله بررسی خواهیم کرد.

آموزش برنامه نویسی با کوئرا کالج
فرهاد رضازاده

ممکن است علاقه‌مند باشید
اشتراک در
اطلاع از
guest

4 دیدگاه‌
قدیمی‌ترین
تازه‌ترین بیشترین واکنش
بازخورد (Feedback) های اینلاین
View all comments
مرتضی
مرتضی
2 سال قبل

خوب بود

کوئرا بلاگ
ادمین
2 سال قبل
پاسخ به  مرتضی

دوست عزیز سلام

خیلی خوشحالیم که این مطلب برای شما مفید بوده

مهدی
مهدی
2 سال قبل

عالی بود
فقط بعضی جاها خیلی جمله ها پیچیده شدن ولی در کل خیلی خوب رفع ابهام کرد

کوئرا بلاگ
ادمین
2 سال قبل
پاسخ به  مهدی

سلام دوست عزیز

خوشحالیم که این مقاله براتون مفید بوده و ممنون که دیدگاهتون رو با ما به اشتراک گذاشتید. سعی می‌کنیم در نگارشِ مقالات آینده حتماً منظور کنیم.