مقایسه Coroutine با Thread
مقایسه Coroutine با Thread
کروتین چیست، ترد چیست و فرق کروتین با ترد کجاست؟
اگر تازه وارد دنیای برنامهنویسی همزمانی یا مهندسی نرم افزار شده باشید، احتمالاً با واژههایی مثل Thread، Coroutine، Green Thread، Async و Concurrency روبهرو شدهاید. در نگاه اول ممکن است همه این مفاهیم شبیه هم به نظر برسند، چون همه آنها به نوعی کمک میکنند برنامه بتواند چند کار را به شکل همزمان یا شبههمزمان انجام دهد. اما در عمل تفاوتهای مهمی میان آنها وجود دارد. برای درک بهتر این موضوع، باید اول بدانیم ترد چیست، کروتین چیست و بعد بررسی کنیم فرق کروتین با ترد در کجاست. همچنین لازم است Green Thread را هم بشناسیم، چون از نظر مفهومی بین Thread واقعی سیستمعامل و Coroutine قرار میگیرد و گاهی باعث سردرگمی برنامهنویسان مبتدی میشود.
ترد چیست؟
وقتی میپرسیم ترد چیست، در سادهترین توضیح میتوان گفت ترد یک مسیر اجرای مستقل درون یک پردازش است. هر برنامهای که اجرا میشود معمولاً یک Process یا پردازش دارد و داخل آن پردازش میتواند یک یا چند Thread وجود داشته باشد. هر Thread میتواند بخشی از کد را اجرا کند و اگر چند Thread داشته باشیم، برنامه میتواند چند کار را تقریباً همزمان انجام دهد. برای مثال، یک برنامه میتواند در یک Thread اطلاعات را از اینترنت دانلود کند، در Thread دیگر رابط کاربری را پاسخگو نگه دارد و در Thread سوم دادهها را پردازش کند. این مدل در بسیاری از زبانها و سیستمها استفاده میشود، اما هزینهها و پیچیدگیهای خودش را دارد.
برای اینکه بهتر بفهمیم ترد چیست، باید به نقش سیستمعامل هم توجه کنیم. Threadهای معمولی که در بسیاری از زبانها استفاده میشوند، اغلب توسط سیستمعامل مدیریت میشوند. یعنی سیستمعامل تصمیم میگیرد چه زمانی یک Thread اجرا شود، چه زمانی متوقف شود و چه زمانی نوبت به Thread دیگری برسد. به این کار زمانبندی یا Scheduling گفته میشود. اگر یک سیستم چند هسته پردازنده داشته باشد، ممکن است چند Thread واقعاً به صورت موازی روی هستههای مختلف اجرا شوند. به همین دلیل Thread برای کارهای سنگین پردازشی میتواند مفید باشد، البته به شرطی که زبان برنامهنویسی و محیط اجرا اجازه استفاده مؤثر از چند هسته را بدهد.
اما Thread همیشه راهحل ساده و بیدردسری نیست. هر Thread به حافظه نیاز دارد، معمولاً Stack جداگانه دارد و ساختن تعداد زیادی Thread میتواند منابع سیستم را مصرف کند. علاوه بر این، چون Threadها ممکن است همزمان به دادههای مشترک دسترسی داشته باشند، مشکلاتی مثل Race Condition، Deadlock و نیاز به Lock به وجود میآید. برای یک برنامهنویس مبتدی، مدیریت Threadها ممکن است دشوار باشد، چون خطاهای مربوط به همزمانی همیشه واضح نیستند و گاهی فقط در شرایط خاصی رخ میدهند. بنابراین وقتی میپرسیم ترد چیست، باید بدانیم که ترد یک ابزار قدرتمند است، اما قدرت آن همراه با مسئولیت و پیچیدگی میآید.
حالا باید ببینیم کروتین چیست. Coroutine یا کروتین یک واحد اجرای سبکتر از Thread است که معمولاً توسط خود زبان برنامهنویسی یا Runtime مدیریت میشود، نه مستقیماً توسط سیستمعامل. کروتینها برای اجرای کارهایی طراحی شدهاند که نیاز دارند در نقطهای متوقف شوند و بعداً از همان نقطه ادامه پیدا کنند. برای مثال، فرض کنید برنامه شما باید از یک API داده بگیرد. در مدل سنتی، ممکن است هنگام انتظار برای پاسخ سرور، Thread بلاک شود و کاری انجام ندهد. اما با کروتین، برنامه میتواند در زمان انتظار، اجرای کروتین را متوقف کند و به کارهای دیگر برسد. وقتی پاسخ آماده شد، همان کروتین از ادامه همان نقطه اجرا میشود.
اگر بخواهیم ساده بگوییم کروتین چیست، میتوان گفت کروتین شبیه یک تابع قابل توقف و ادامهدادن است. تابع معمولی وقتی اجرا میشود، از ابتدا شروع میکند و تا انتها پیش میرود، مگر اینکه خطایی رخ دهد یا مقدار برگرداند. اما کروتین میتواند در میان اجرای خود مکث کند، کنترل را به بخش دیگری از برنامه بدهد و بعداً دوباره از همان نقطه ادامه پیدا کند. همین ویژگی باعث میشود Coroutine برای برنامههایی که عملیات ورودی و خروجی زیادی دارند، مثل درخواست شبکه، خواندن فایل، کار با دیتابیس یا ارتباط با سرویسهای خارجی، بسیار مفید باشد.
فرق کروتین با ترد از همینجا شروع میشود. Thread معمولاً توسط سیستمعامل زمانبندی میشود و میتواند به شکل پیشدستانه یا Preemptive متوقف شود. یعنی سیستمعامل میتواند هر لحظه اجرای یک Thread را قطع کند و Thread دیگری را اجرا کند. اما Coroutine معمولاً به شکل همکاریمحور یا Cooperative کار میکند. یعنی خود کروتین باید در نقطهای مشخص اعلام کند که فعلاً منتظر است و میتواند کنترل را واگذار کند. این تفاوت بسیار مهم است، چون باعث میشود رفتار Coroutine قابل پیشبینیتر باشد و هزینه جابهجایی بین کارها کمتر شود.
در بحث فرق کروتین با ترد، یکی از مهمترین موضوعات مصرف منابع است. Threadها معمولاً سنگینتر هستند، چون هر Thread منابعی از سیستمعامل میگیرد و مدیریت آن هزینه دارد. در مقابل، کروتینها بسیار سبکتر هستند و در بسیاری از زبانها میتوان هزاران یا حتی میلیونها Coroutine ایجاد کرد، در حالی که ایجاد همین تعداد Thread واقعی ممکن است سیستم را با مشکل جدی روبهرو کند. به همین دلیل در برنامههای مدرن، بهخصوص سرورها و سرویسهایی که باید تعداد زیادی درخواست همزمان را مدیریت کنند، Coroutineها محبوبیت زیادی پیدا کردهاند.
برای مثال، فرض کنید یک وبسرور دارید که باید به ده هزار کاربر همزمان پاسخ دهد. اگر برای هر کاربر یک Thread جداگانه ایجاد کنید، سیستم ممکن است با مصرف زیاد حافظه، هزینه بالای زمانبندی و کندی مواجه شود. اما اگر از Coroutine استفاده کنید، هر درخواست میتواند به شکل یک کروتین مدیریت شود. وقتی یک درخواست منتظر پاسخ دیتابیس یا سرویس خارجی است، Coroutine مربوط به آن متوقف میشود و Runtime میتواند به Coroutineهای دیگر رسیدگی کند. در چنین شرایطی فرق کروتین با ترد کاملاً محسوس میشود، چون Coroutineها کمک میکنند با منابع کمتر، تعداد بیشتری کار همزمان مدیریت شود.
البته نباید فکر کنیم Coroutine همیشه جایگزین کامل Thread است. برای درک دقیقتر اینکه کروتین چیست، باید محدودیتهای آن را هم بشناسیم. Coroutineها معمولاً برای کارهای I/O-bound عالی هستند، یعنی کارهایی که بیشتر زمان آنها صرف انتظار برای ورودی و خروجی میشود. اما اگر کاری شدیداً CPU-bound باشد، یعنی بیشتر زمان آن صرف محاسبات سنگین پردازنده شود، Coroutine بهتنهایی الزاماً باعث اجرای موازی روی چند هسته نمیشود. در بسیاری از Runtimeها، Coroutineها روی تعداد محدودی Thread اجرا میشوند و اگر یک Coroutine محاسبات سنگین انجام دهد و کنترل را آزاد نکند، میتواند اجرای سایر Coroutineها را کند کند.
در مقابل، Thread برای کارهای CPU-bound میتواند مناسبتر باشد، چون Threadهای واقعی سیستمعامل این امکان را دارند که روی هستههای مختلف پردازنده اجرا شوند. البته این موضوع به زبان برنامهنویسی هم بستگی دارد. برای مثال، در بعضی محیطها محدودیتهایی مثل Global Interpreter Lock وجود دارد که باعث میشود Threadها برای پردازش موازی واقعی محدود شوند. اما از نظر مفهومی، Thread ابزار طبیعیتری برای موازیسازی پردازشهای سنگین است، در حالی که Coroutine ابزار طبیعیتری برای مدیریت همزمانی سبک و کارهای منتظرماندنی است.
حالا نوبت به Green Thread میرسد. Green Thread چیست و چه فرقی با Coroutine و Thread دارد؟ Green Thread در واقع نوعی Thread است که توسط Runtime زبان برنامهنویسی مدیریت میشود، نه مستقیماً توسط سیستمعامل. از بیرون ممکن است شبیه Thread به نظر برسد، چون برنامهنویس میتواند چند مسیر اجرا داشته باشد، اما زمانبندی آنها توسط ماشین مجازی، Runtime یا کتابخانه زبان انجام میشود. به همین دلیل Green Thread از نظر مدیریت به Coroutine نزدیکتر است، اما از نظر مدل ذهنی میتواند شبیه Thread باشد.
برای فهم بهتر Green Thread، تصور کنید سیستمعامل فقط یک یا چند Thread واقعی به برنامه داده است، اما Runtime زبان داخل همان Threadها تعداد زیادی Thread سبکتر ایجاد میکند. این Threadهای سبک توسط خود Runtime جابهجا و مدیریت میشوند. در این مدل، هزینه ساخت و جابهجایی معمولاً کمتر از Thread واقعی سیستمعامل است. بعضی زبانها در گذشته یا حالا از مدلهایی شبیه Green Thread استفاده کردهاند. برای مثال، Goroutine در زبان Go گاهی با Green Thread مقایسه میشود، هرچند پیادهسازی آن ویژگیهای خاص خودش را دارد. Fiberها در بعضی زبانها هم به این خانواده مفهومی نزدیک هستند.
تفاوت Coroutine با Green Thread
تفاوت Coroutine با Green Thread کمی ظریف است. Coroutine معمولاً به توقف و ادامه داوطلبانه وابسته است. یعنی کد باید در نقاط مشخصی مثل await یا yield کنترل را واگذار کند. اما Green Thread میتواند در بعضی پیادهسازیها رفتاری شبیه Thread داشته باشد و Runtime خودش زمانبندی را انجام دهد. البته این بستگی به زبان و پیادهسازی دارد. در بسیاری از موارد، هم Coroutine و هم Green Thread سبکتر از Thread سیستمعامل هستند، اما Coroutine بیشتر به عنوان ساختاری برای برنامهنویسی ناهمزمان شناخته میشود، در حالی که Green Thread بیشتر تلاش میکند تجربهای شبیه Thread اما سبکتر ارائه دهد.
در بحث فرق کروتین با ترد، Green Thread مثل یک مفهوم میانی عمل میکند. Thread واقعی معمولاً سنگینتر است و سیستمعامل آن را مدیریت میکند. Coroutine معمولاً سبکتر است و در نقاط مشخصی کنترل را واگذار میکند. Green Thread هم معمولاً سبکتر از Thread واقعی است، اما میخواهد مدل Threadمانند را در سطح Runtime پیادهسازی کند. بنابراین اگر بخواهیم خیلی ساده بگوییم، Thread واقعی به سیستمعامل وابسته است، Coroutine به همکاری کد و Runtime وابسته است و Green Thread به زمانبندی Runtime زبان وابسته است.
یکی از دلایلی که Coroutine برای مبتدیها جذاب است، خوانایی کد است. در گذشته برای انجام کارهای ناهمزمان، برنامهنویسان مجبور بودند از Callbackهای تو در تو استفاده کنند. این روش باعث میشد کد پیچیده و سختخوان شود. اما Coroutineها با استفاده از ساختارهایی مثل async و await کاری میکنند که کد ناهمزمان شبیه کد خطی و ساده نوشته شود. یعنی شما میتوانید بنویسید ابتدا درخواست را ارسال کن، بعد منتظر پاسخ بمان، بعد نتیجه را پردازش کن؛ در حالی که پشت صحنه، Runtime هنگام انتظار، Thread را بلاک نمیکند و به کارهای دیگر میرسد. این یکی از مهمترین مزیتهای Coroutine است.
با این حال، فهمیدن کروتین چیست بدون شناخت مفهوم بلاک شدن کامل نیست. وقتی یک Thread بلاک میشود، یعنی تا زمانی که نتیجه آماده نشود، آن Thread نمیتواند کار دیگری انجام دهد. اگر تعداد Threadها محدود باشد، بلاک شدن زیاد میتواند باعث کاهش کارایی شود. اما وقتی یک Coroutine منتظر میماند، معمولاً خود Thread آزاد میشود تا Coroutine دیگری را اجرا کند. به همین دلیل در کارهایی مثل وباسکرپینگ، ارتباط با دیتابیس، ساخت APIهای پرکاربر یا برنامههای چت و پیامرسان، Coroutineها میتوانند عملکرد بسیار خوبی ارائه دهند.
از طرف دیگر، Threadها هنوز هم جایگاه مهمی دارند. اگر برنامهای دارید که باید تصویر پردازش کند، الگوریتمهای سنگین اجرا کند، دادههای حجیم را تحلیل کند یا چند محاسبه سنگین را همزمان روی چند هسته انجام دهد، Thread یا حتی Process میتواند انتخاب مناسبی باشد. در اینجا فرق کروتین با ترد به نوع کار مربوط میشود. Coroutine برای انتظارهای زیاد و کارهای سبک عالی است، اما Thread برای اجرای واقعی همزمان روی چند هسته کاربرد بیشتری دارد. بنابراین انتخاب بین Coroutine و Thread نباید فقط بر اساس مد روز باشد، بلکه باید بر اساس نوع مسئله انجام شود.
موضوع دیگر، خطایابی و مدیریت ذهنی است. Threadها به دلیل اجرای همزمان واقعی ممکن است رفتارهای غیرقابل پیشبینیتری داشته باشند. اگر دو Thread همزمان یک متغیر مشترک را تغییر دهند، نتیجه ممکن است به ترتیب اجرای آنها بستگی داشته باشد. این موضوع باعث خطاهای پیچیده میشود. در Coroutineها چون نقاط تغییر کنترل معمولاً مشخصتر هستند، کنترل ذهنی برنامه سادهتر میشود. البته Coroutineها هم میتوانند خطاهای خاص خودشان را داشته باشند، مخصوصاً وقتی برنامهنویس فراموش کند یک عملیات را await کند یا یک کار سنگین را داخل Coroutine اجرا کند و کل Event Loop را قفل کند.
برای مخاطب مبتدی، شاید بهترین تشبیه این باشد که Thread مثل چند کارمند واقعی است که هرکدام میتوانند مستقل کار کنند و حتی همزمان روی میزهای مختلف بنشینند. اما استخدام و مدیریت این کارمندان هزینه دارد و اگر بدون هماهنگی به یک پرونده مشترک دست بزنند، ممکن است آشفتگی ایجاد شود. Coroutine شبیه یک کارمند بسیار منظم است که وقتی منتظر جواب تلفن یا ایمیل است، پرونده را کنار میگذارد و سراغ پرونده بعدی میرود. او لزوماً چند کار را دقیقاً در یک لحظه انجام نمیدهد، اما زمانهای مرده را خیلی خوب مدیریت میکند. Green Thread هم شبیه کارمندانی مجازی است که مدیر داخلی شرکت آنها را بین میزهای محدود جابهجا میکند، بدون اینکه سیستم بیرونی از جزئیات همه آنها خبر داشته باشد.
در نهایت، اگر بخواهیم پاسخ روشنی به این سه سؤال بدهیم که کروتین چیست، ترد چیست و فرق کروتین با ترد چیست، باید بگوییم ترد یک مسیر اجرای مستقلتر و سنگینتر است که معمولاً توسط سیستمعامل مدیریت میشود و برای اجرای موازی واقعی مناسبتر است. کروتین یک واحد اجرای سبکتر است که معمولاً توسط Runtime مدیریت میشود و برای کارهای ناهمزمان و I/O-bound بسیار مناسب است. فرق کروتین با ترد در مدیریت، مصرف منابع، نوع زمانبندی، میزان پیچیدگی، کاربرد و توانایی اجرای موازی واقعی دیده میشود. Green Thread هم مفهومی میان این دو است که تلاش میکند Threadهای سبک و مدیریتشده توسط Runtime ارائه دهد.
بنابراین اگر در حال ساخت یک API، وبسرور، ربات، برنامه شبکهای یا سیستمی هستید که بیشتر زمان آن صرف انتظار برای پاسخ دیتابیس، فایل یا اینترنت میشود، Coroutine میتواند انتخاب بسیار خوبی باشد. اما اگر برنامه شما نیاز به محاسبات سنگین و استفاده جدی از چند هسته پردازنده دارد، Thread یا Process احتمالاً انتخاب مناسبتری است. مهمترین نکته این است که هیچکدام از این ابزارها ذاتاً بهتر از دیگری نیستند. هرکدام برای مسئلهای خاص طراحی شدهاند. وقتی بدانید کروتین چیست، ترد چیست و فرق کروتین با ترد دقیقاً کجاست، میتوانید در پروژههای واقعی تصمیمهای فنی بهتری بگیرید و کدی بنویسید که هم خواناتر باشد، هم سریعتر و هم قابل نگهداریتر. اگر حس میکنید مفایهم مطرح شده کمی گنگ است حتما در آموزش و یادگیری Flask به عنوان یکی از فریمورک های مطرح پایتون حتما به نیاز همزمانی پی خواهید برد.