حلقه رویداد در برابر مسدودسازی رشته: تفاوت عملکرد در ورودی/خروجی همزمان و ناهمزمان
در حالی که حلقههای رویداد به سرورها اجازه میدهند هزاران اتصال همزمان را با حداقل حافظه مدیریت کنند، اما در برابر وظایف وابسته به پردازنده (CPU-bound) بسیار آسیبپذیر باقی میمانند. درک شکاف معماری بین حلقههای رویداد ناهمزمان و استخرهای رشتهای همزمان برای مقیاسپذیری برنامههای وب مدرن حیاتی است.
به قلم غزل بختیاری
این خبر را به اشتراک بگذارید
- مدافعان رویدادمحور
- استدلال میکنند که حلقههای رویداد تکرشتهای مقیاسپذیری و کارایی حافظه برتری را برای برنامههای شبکه وابسته به ورودی/خروجی فراهم میکنند.
- طرفداران چندرشتهای
- معتقدند که مدلهای «رشته به ازای هر اتصال» استدلال کد سادهتری و عملکرد بهتری برای وظایف وابسته به پردازنده ارائه میدهند.
- عملگرایان معماری ترکیبی
- باور دارند که سیستمهای مدرن به ترکیبی از حلقههای رویداد برای شبکهسازی و استخرهای رشتهای برای محاسبات سنگین نیاز دارند.
نکات کلیدی
- مدلهای سنتی «رشته به ازای هر اتصال» تقریباً ۱ مگابایت حافظه به ازای هر کاربر مصرف میکنند که باعث تورم حافظه در مقیاس بالا میشود.
- حلقههای رویداد از یک رشته اصلی واحد برای مدیریت هزاران اتصال استفاده میکنند و برای اطلاع از اتمام وظایف ورودی/خروجی به سیستم عامل متکی هستند.
- حلقههای رویداد، در حالی که برای درخواستهای شبکه بسیار کارآمد هستند، اگر مجبور به اجرای محاسبات سنگین وابسته به پردازنده شوند، کاملاً متوقف میشوند.
- محیطهای اجرایی مدرن مانند Node.js از استخرهای رشته کاری پنهان استفاده میکنند تا عملیاتهای مسدودکننده را بدون توقف حلقه اصلی مدیریت کنند.
- رشتههای مجازی جاوا یک رویکرد ترکیبی ارائه میدهند و میلیونها رشته سبکوزن را روی یک استخر کوچک از رشتههای سیستم عامل نگاشت میکنند.
یک معماری ناهمزمان تنها زمانی دوام میآورد که هر وظیفهای که پردازش میکند تقریباً فوراً به پایان برسد. به محض اینکه یک عملیات مسیر اجرای اصلی را مسدود کند—چه یک هش رمزنگاری پیچیده باشد، چه تجزیه یک محموله عظیم JSON، یا یک کوئری ضعیف پایگاه داده—کل سیستم متوقف میشود و هزاران کاربر دیگر پشت سر یک درخواست کُند صف میکشند. این محدودیت اصلی حلقه رویداد است، مدلی از همزمانی که همه چیز را از موتور V8 گوگل در مرورگرها گرفته تا بکاند Node.js و NGINX قدرت میدهد. فروشندگان نرمافزار اغلب حلقه رویداد را به عنوان یک راهحل جادویی برای مقیاسپذیری نامحدود میفروشند و وعده میدهند که یک رشته (Thread) واحد میتواند ۱۰,۰۰۰ اتصال همزمان را بدون تورم حافظه مدلهای چندرشتهای سنتی مدیریت کند. اما این وعده تنها زمانی صادق است که بار کاری صرفاً محدود به تأخیر شبکه یا دیسک باشد، نه پردازنده (CPU). اگر سختافزار مجبور باشد به جای انتظار برای یک بسته شبکه، واقعاً یک نتیجه را محاسبه کند، توهم تکرشتهای بودن در هم میشکند.[1][6]
برای درک اینکه چرا صنعت نرمافزار این محدودیت شکننده را پذیرفته است، باید به جایگزین آن نگاه کرد. در یک معماری همزمان سنتی، به هر اتصال شبکه ورودی، یک رشته سیستم عامل اختصاصی تخصیص داده میشود. این مدل، که به طور مشهور توسط نسخههای اولیه سرور Apache HTTP استفاده میشد، برای توسعهدهندگان بسیار ساده و قابل درک است. کد خط به خط اجرا میشود، و اگر یک کوئری پایگاه داده ۲۰۰ میلیثانیه طول بکشد تا برگردد، آن رشته خاص به سادگی به خواب میرود تا زمانی که دادهها برسند. سیستم عامل مدیریت تعویض زمینه (Context Switching) را بر عهده میگیرد و رشته بیکار را متوقف کرده و رشته دیگری را که کار برای انجام دارد بیدار میکند. با این حال، این سادگی با یک هزینه سختافزاری سنگین همراه است که با مقیاسپذیری اینترنت آشکار شد.[2][4]
محاسبات معماریهای «رشته به ازای هر اتصال» در شرایط همزمانی بالا از کار میافتد. به طور پیشفرض، یک رشته استاندارد جاوا تقریباً ۱ مگابایت حافظه فقط برای پشته اجرای خود مصرف میکند. اگر یک سرور نیاز به مدیریت ۱۰,۰۰۰ اتصال همزمان داشته باشد—معیاری که به عنوان مشکل C10K شناخته میشود—برنامه به ۱۰ گیگابایت رم صرفاً برای سربار رشتهها نیاز دارد، قبل از اینکه حتی یک بایت داده واقعی برنامه پردازش شود. علاوه بر این، هسته سیستم عامل باید دائماً بین این هزاران رشته تعویض زمینه انجام دهد، فرآیندی که چرخههای پردازنده را صرفاً برای تعیین اینکه کدام رشته باید در مرحله بعد اجرا شود، میسوزاند. این تورم حافظه و سربار تعویض زمینه، یک سقف سخت برای تعداد کاربرانی که یک سرور فیزیکی واحد میتوانست پشتیبانی کند، ایجاد کرد.[3][5]
حلقه رویداد به طور خاص برای دور زدن این گلوگاه حافظه طراحی شد. به جای ایجاد یک رشته جدید برای هر کاربر، یک معماری رویدادمحور از یک رشته اصلی واحد استفاده میکند که به طور مداوم در یک حلقه اجرا میشود. هنگامی که درخواستی میرسد، رشته عملیات ورودی/خروجی لازم—مانند خواندن یک فایل یا کوئری پایگاه داده—را آغاز میکند و بلافاصله یک تابع فراخوان (Callback) را ثبت میکند. این رشته منتظر نمیماند تا عملیات به پایان برسد. در عوض، انتظار را به APIهای چندگانهسازی (Multiplexing) سیستم عامل، مانند epoll در لینوکس یا kqueue در macOS، واگذار میکند و فوراً به سراغ درخواست کاربر بعدی میرود. هنگامی که پایگاه داده در نهایت پاسخ میدهد، سیستم عامل حلقه رویداد را مطلع میکند، که سپس فراخوان ثبت شده را برای تکمیل تراکنش اجرا میکند.[4][6]
این رویکرد غیرمسدودکننده، بازدهی کارایی فوقالعادهای برای بارهای کاری خاص به ارمغان میآورد. از آنجایی که تنها یک رشته درخواستهای شبکه را مدیریت میکند، مالیات حافظه ۱ مگابایتی به ازای هر اتصال کاملاً از بین میرود. یک سرور Node.js میتواند به راحتی ۱۰,۰۰۰ اتصال همزمان را با استفاده از کمتر از ۵۰ مگابایت سربار حافظه کلی مدیریت کند. سیستم عامل دیگر با هزاران تعویض زمینه سنگین نمیشود و به پردازنده اجازه میدهد تقریباً ۱۰۰ درصد چرخههای خود را صرف اجرای منطق واقعی برنامه کند، نه مدیریت وضعیت رشتهها. این قابلیتی است که سرورهای رویدادمحور مانند NGINX را از Apache پیش انداخت و پایه و اساس تسلط جاوا اسکریپت در توسعه وب مدرن است.[1][3]
این رویکرد غیرمسدودکننده، بازدهی کارایی فوقالعادهای برای بارهای کاری خاص به ارمغان میآورد.
با این حال، زبان بازاریابی پیرامون «ورودی/خروجی غیرمسدودکننده» اغلب واقعیت معماری را پنهان میکند. در حالی که خود عملیاتهای ورودی/خروجی رشته را مسدود نمیکنند، کدهای برنامه که فراخوانها را اجرا میکنند قطعاً این کار را انجام میدهند. مستندات Node.js به صراحت در مورد این آسیبپذیری به توسعهدهندگان هشدار میدهد: «حلقه رویداد چیزی است که به Node.js اجازه میدهد عملیات ورودی/خروجی غیرمسدودکننده را انجام دهد.» اما اگر یک توسعهدهنده یک حلقه همزمان بنویسد که ۵ ثانیه طول بکشد تا یک عدد اول را محاسبه کند، حلقه رویداد نمیتواند هیچ فراخوان دیگری را در طول آن ۵ ثانیه پردازش کند. هر کاربر دیگری که به آن سرور متصل است، یک توقف کامل را تجربه خواهد کرد و منتظر میماند تا یک وظیفه وابسته به پردازنده، چنگال خود را از رشته اصلی رها کند.[1][6]
این آسیبپذیری بحثهای شدید آکادمیک و مهندسی را در طول سالها برانگیخته است. در سال ۲۰۱۶، محققان و مهندسان به شدت پارادایم رویدادمحور را مورد بررسی قرار دادند. راب فون بهرن، در مقالهای که به طور گسترده مورد استناد قرار گرفت و توسط TheTechSolo تحلیل شد، علیه تبلیغات رایج استدلال کرد. او نوشت: «به طور خاص، ما معتقدیم که رشتهها میتوانند به تمام نقاط قوت رویدادها دست یابند، از جمله پشتیبانی از همزمانی بالا، سربار کم، و یک مدل همزمانی ساده.» استدلال این بود که حلقههای رویداد توسعهدهندگان را مجبور میکنند تا کدهای تکهتکه و پر از فراخوان بنویسند تا فقط محدودیتهای یک رشته واحد را دور بزنند، در حالی که بار مدیریت وضعیت را به طور کامل به لایه برنامه منتقل میکنند.[5]
برای کاهش دام وظایف وابسته به پردازنده، محیطهای اجرایی رویدادمحور مدرن به آرامی تقلب میکنند. آنها در واقع تکرشتهای نیستند. به عنوان مثال، Node.js به یک کتابخانه C به نام libuv متکی است که یک استخر پنهان از رشتههای کاری را در پسزمینه نگهداری میکند. هنگامی که یک برنامه Node.js با یک عملیات واقعاً مسدودکننده مواجه میشود که نمیتواند به صورت ناهمزمان توسط سیستم عامل مدیریت شود—مانند عملیاتهای پیچیده سیستم فایل یا هش رمزنگاری—به طور مخفیانه آن کار را به یکی از رشتههای استخر کاری libuv واگذار میکند. حلقه رویداد به چرخش خود ادامه میدهد و توهم اجرای غیرمسدودکننده تکرشتهای را حفظ میکند، در حالی که چندرشتهای سنتی کار سنگین را در پشت صحنه انجام میدهد.[1][4]
شکاف معماری بین این دو مدل اکنون از جهت دیگر در حال بسته شدن است. تا دسامبر ۲۰۲۵، اکوسیستم جاوا به طور کامل Project Loom را ادغام کرده بود و «رشتههای مجازی» (Virtual Threads) را به ماشین مجازی جاوا (JVM) معرفی کرد. رشتههای مجازی تلاش میکنند بهترینهای هر دو دنیا را ارائه دهند: آنها به توسعهدهندگان اجازه میدهند کدهای ساده، همزمان و مسدودکننده بنویسند، اما JVM میلیونها از این رشتههای مجازی سبکوزن را روی یک استخر کوچک از رشتههای واقعی سیستم عامل نگاشت میکند. هنگامی که یک رشته مجازی در یک عملیات ورودی/خروجی مسدود میشود، JVM به طور خودکار آن را تعلیق میکند و رشته حامل زیرین را به وظیفه دیگری واگذار میکند، که کارایی حلقه رویداد را بدون مجبور کردن توسعهدهنده به نوشتن فراخوانهای ناهمزمان تقلید میکند.[3]
این همگرایی یک حقیقت اساسی در مورد معماری نرمافزار را برجسته میکند: پیچیدگی را نمیتوان از بین برد، بلکه فقط میتوان آن را جابجا کرد. مدل حلقه رویداد، که توسط جاوا اسکریپت حمایت میشود، محیط زمان اجرا را نسبتاً ساده نگه میدارد اما توسعهدهنده برنامه را مجبور میکند تا وضعیت ناهمزمان را با دقت مدیریت کند و از عملیاتهای مسدودکننده پردازنده اجتناب ورزد. مدل رشته مجازی، که توسط جاوا مدرن حمایت میشود، به توسعهدهنده اجازه میدهد کد دستوری ساده بنویسد، اما به یک محیط زمان اجرای فوقالعاده پیچیده و مهندسی شده نیاز دارد تا زمانبندی رشتههای سبکوزن و تعویض زمینه را در زیرساخت مدیریت کند.[3][4]
برای تیمهای مهندسی که امروز تصمیمات زیرساختی میگیرند، انتخاب بین این مدلها کاملاً به مشخصات بار کاری بستگی دارد. اگر یک برنامه صرفاً وابسته به ورودی/خروجی (I/O-bound) باشد—مانند یک دروازه API که هزاران محموله JSON سبکوزن را مسیریابی میکند، یا یک سرور چت بلادرنگ که هزاران اتصال WebSocket بیکار را مدیریت میکند—حلقه رویداد خالص در کارایی حافظه و تأخیر قابل پیشبینی بیرقیب باقی میماند. اما اگر برنامه به بارهای کاری ترکیبی نیاز داشته باشد، جایی که درخواستهای شبکه اغلب پردازش دادههای سنگین، دستکاری تصویر یا منطق تجاری پیچیده را فعال میکنند، یک مدل مبتنی بر رشته، انزوای لازم را فراهم میکند تا از تخریب کل سیستم توسط یک درخواست سنگین جلوگیری کند.[2][6]
دوران برخورد با حلقه رویداد به عنوان یک معماری برتر جهانی به پایان رسیده است. همانطور که تعداد هستههای سختافزاری همچنان افزایش مییابد و محیطهای زمان اجرا در مدیریت رشتههای سبکوزن پیچیدهتر میشوند، صنعت به سمت رویکردهای ترکیبی حرکت میکند. مقاومترین سیستمها اکنون محدودیت اصلی حلقه رویداد را میپذیرند و از آن صرفاً برای چندگانهسازی شبکه استفاده میکنند، در حالی که هر کار محاسباتی را به شدت به استخرهای رشتهای ایزوله واگذار میکنند. تفاوت عملکرد دیگر در این نیست که یک چارچوب ادعا میکند از کدام مدل استفاده میکند، بلکه در این است که چقدر مؤثر از برخورد این دو پارادایم جلوگیری میکند.[1][3]
اصطلاحات کلیدی
- حلقه رویداد
- یک ساختار برنامهنویسی که منتظر رویدادها یا پیامها در یک برنامه میماند و آنها را توزیع میکند، و به یک رشته واحد اجازه میدهد چندین عملیات همزمان را مدیریت کند.
- تعویض زمینه
- فرآیندی که در آن یک سیستم عامل وضعیت یک رشته را ذخیره میکند و وضعیت رشته دیگری را بارگذاری میکند، که منابع پردازنده را مصرف میکند.
- وابسته به ورودی/خروجی
- وضعیتی که در آن عملکرد یک برنامه توسط سرعت عملیات ورودی/خروجی، مانند درخواستهای شبکه یا خواندن دیسک، محدود میشود، نه سرعت پردازنده.
- وابسته به پردازنده
- وضعیتی که در آن عملکرد یک برنامه توسط سرعت پردازنده محدود میشود، معمولاً در طول وظایف سنگین ریاضی یا پردازش داده.
- چندگانهسازی
- روشی که توسط سیستمهای عامل (مانند epoll یا kqueue) برای نظارت همزمان بر چندین اتصال شبکه و اطلاعرسانی به برنامه هنگام آماده شدن دادهها استفاده میشود.
منابع
[1]Node.js Learnمدافعان رویدادمحورDon't Block the Event Loop (or the Worker Pool)
مطالعه در Node.js Learn →
[2]Java Code Geeksعملگرایان معماری ترکیبیScalable I/O: Events- Vs Multithreading-based
مطالعه در Java Code Geeks →
[3]Java Code Geeksعملگرایان معماری ترکیبیThe Async Divide: Java's Virtual Threads vs JavaScript's Event Loop
مطالعه در Java Code Geeks →
[4]Cornell Universityطرفداران چندرشتهایConcurrency, Threads, and Events
مطالعه در Cornell University →
[5]TheTechSoloطرفداران چندرشتهایScalable I/O: Events- Vs Multithreading-based
مطالعه در TheTechSolo →
[6]Wikipediaمدافعان رویدادمحورEvent loop
مطالعه در Wikipedia →
[7]تیم سردبیری کوهستانعملگرایان معماری ترکیبیتحلیل تیم سردبیری کوهستان
مطالعه در تیم سردبیری کوهستان →
نظرات
بیشتر در فناوری
مشاهده همه →سیستمهای پیشرانش
مقایسه چرخههای موتور موشک: بدهبستانها در نیروی رانش، بازدهی و پیچیدگی
6 منبع
فناوری باتری
چرا باتریهای حالت جامد اتصال کوتاه میکنند و چگونه تنش مکانیکی و لایههای میانی جلوی دندریتهای لیتیومی را میگیرند
6 منبع
امنیت وب
کالبدشکافی کپچای نامرئی: سایتها چگونه بدون هیچ تستی میفهمند شما ربات نیستید؟
4 منبع
هر زاویه. هر روز.
دریافت فناوری اخبار همراه با پوشش کامل منابع و تحلیل دیدگاهها، مستقیم در صندوق ورودی شما.




