ناهمزمانی در هر زبان برنامه نویسی سخت است. مفاهیمی مانند همزمانی ، موازی کاری و بن بست ها حتی باتجربه ترین مهندسان را نیز به لرزه می اندازد. کدی که به طور هم زمان اجرا شود غیر قابل پیش بینی است و در هنگام وجود اشکال ، ردیابی آن دشوار است. مشکل غیر قابل گریز است زیرا محاسبات مدرن دارای چندین هسته است. در هر هسته پردازنده محدودیت حرارتی وجود دارد و هیچ چیز سریعتر نمی شود. این توسعه دهنده را تحت فشار قرار می دهد تا کدی کارآمد بنویسد که از سخت افزار بهره می برد.

جاوا اسکریپت تک رشته ای است ، اما آیا این گره استفاده از معماری مدرن را محدود می کند؟ یکی از بزرگترین چالش ها برخورد با چندین رشته به دلیل پیچیدگی ذاتی آن است. چرخاندن رشته های جدید و مدیریت سوئیچ زمینه در این بین گران است. هم سیستم عامل و هم برنامه نویس باید کارهای زیادی انجام دهند تا راه حلی را ارائه دهند که دارای موارد لبه زیادی باشد. در این بررسی ، من نحوه برخورد Node با این باتلاق را از طریق حلقه رویداد به شما نشان خواهم داد. من هر قسمت از حلقه رویداد Node.js را بررسی می کنم و نحوه عملکرد آن را نشان می دهم. یکی از ویژگی های “برنامه قاتل” در Node این حلقه است ، زیرا یک مشکل اساسی را به روشی کاملاً جدید حل می کند.

Event Loop چیست؟

حلقه رویداد یک حلقه تک رشته ای ، غیر مسدود کننده و همزمان به طور همزمان است. برای کسانی که مدرک علوم کامپیوتر ندارند ، یک درخواست وب را که جستجوی پایگاه داده را انجام می دهد ، تصور کنید. یک نخ تنها می تواند هر بار یک کار را انجام دهد. به جای انتظار برای پاسخگویی در پایگاه داده ، همچنان کارهای دیگر را در صف انجام می دهد. در حلقه رویداد ، حلقه اصلی پشته تماس را باز می کند و منتظر بازگشت نیست. از آنجا که این حلقه مسدود نشده است ، کار در یک بار بر روی بیش از یک درخواست وب رایگان است. چندین درخواست به طور همزمان می توانند در صف قرار بگیرند ، که این امر باعث می شود که همزمان باشد. این حلقه برای تکمیل همه درخواست ها از یک درخواست منتظر نمی ماند ، اما تماس ها را بدون اینکه مسدود شوید ، دریافت می کند.

این حلقه خود نیمه بی نهایت است ، به این معنی که اگر پشته تماس یا صف پاسخگویی خالی باشد ، می تواند از حلقه خارج شود. پشته تماس را به عنوان یک کد همزمان در نظر بگیرید ، مانند console.log، قبل از حلقه نظرسنجی برای کار بیشتر. Node از libuv تحت پوشش برای نظرسنجی سیستم عامل برای برگشت تماس از اتصالات ورودی استفاده می کند.

شاید از خود بپرسید که چرا حلقه رویداد در یک موضوع اجرا می شود؟ رشته ها با توجه به داده های مورد نیاز در هر اتصال نسبتاً سنگین هستند. رشته ها منابع سیستم عاملی هستند که چرخش می یابند و این به هزاران اتصال فعال تبدیل نمی شود.

چندین موضوع به طور کلی نیز داستان را پیچیده می کند. اگر بازخوانی با داده برگردد ، باید زمینه را به موضوع اجرا بازگرداند. جابجایی زمینه بین رشته ها کند است ، زیرا باید حالت فعلی را مانند پشته تماس یا متغیرهای محلی هماهنگ کند. حلقه رویداد وقتی چندین رشته منابع را به اشتراک می گذارد اشکالات را خرد می کند ، زیرا تک رشته است. یک حلقه تک رشته ای موارد لبه ایمنی نخ را برش می دهد و می تواند خیلی سریع سوئیچ زمینه را انجام دهد. این نبوغ واقعی پشت حلقه است. ضمن اینکه مقیاس پذیر است ، از اتصالات و نخ ها به طور م effectiveثر استفاده می کند.

نظریه کافی است؛ وقت آن است که ببینیم این کد به صورت ظاهری چگونه است در صورت تمایل می توانید در یک REPL یا کد منبع را بارگیری کنید.

حلقه نیمه بی نهایت

بزرگترین سوالی که حلقه رویداد باید پاسخ دهد این است که آیا حلقه زنده است؟ در این صورت ، مشخص می شود که چه مدت باید در صف مکالمه منتظر بمانید. در هر تکرار ، حلقه پشته تماس را باز می کند و سپس نظرسنجی می کند.

در اینجا مثالی آورده شده است که حلقه اصلی را مسدود می کند:

setTimeout(
  () => console.log('Hi from the callback queue'),
  5000); 

const stopTime = Date.now() + 2000;
while (Date.now() < stopTime) {} 

اگر این کد را اجرا کردید ، توجه داشته باشید که حلقه به مدت دو ثانیه مسدود می شود. اما این حلقه زنده می ماند تا اینکه پاسخ در عرض پنج ثانیه اجرا شود. هنگامی که حلقه اصلی رفع انسداد شد ، مکانیزم نظرسنجی مشخص می کند که چه مدت در برگشت پاسخ منتظر است. این حلقه با باز شدن پشته تماس از بین می رود و دیگر هیچ برگشتی باقی نمی ماند.

صف تماس

حال ، چه اتفاقی می افتد که من حلقه اصلی را مسدود کرده و سپس یک تماس مجدد را برنامه ریزی می کنم؟ پس از مسدود شدن حلقه ، تماس های بیشتری را در صف قرار نمی دهد:

const stopTime = Date.now() + 2000;
while (Date.now() < stopTime) {} 


setTimeout(() => console.log('Ran callback A'), 5000);

این بار این حلقه به مدت هفت ثانیه زنده می ماند. حلقه رویداد در سادگی خود گنگ است. این راهی ندارد که بداند در آینده چه صفهایی به وجود می آید. در یک سیستم واقعی ، تماس های برگشتی صف می گیرند و اجرا می شوند زیرا حلقه اصلی برای نظرسنجی آزاد است. حلقه رویداد چندین مرحله را طی می کند به ترتیب وقتی رفع انسداد می شود. بنابراین ، برای دستیابی به مصاحبه شغلی مربوط به حلقه ، از اصطلاحات زیبایی مانند “ساطع کننده رویداد” یا “الگوی راکتور” خودداری کنید. این یک حلقه فروتن تک رشته ای ، همزمان و غیر مسدود کننده است.

حلقه رویداد با async / در انتظار

برای جلوگیری از مسدود کردن حلقه اصلی ، یک ایده این است که ورودی / خروجی همزمان را به صورت async / انتظار منتقل کنید:

const fs = require('fs');
const readFileSync = async (path) => await fs.readFileSync(path);

readFileSync('readme.md').then((data) => console.log(data));
console.log('The event loop continues without blocking...');

هر چیزی که بعد از await از صف پاسخگویی ناشی می شود. این کد مانند کد مسدود کردن همزمان خوانده می شود ، اما مسدود نمی شود. توجه داشته باشید همگام سازی / در انتظار ایجاد می کند readFileSync قابل استفاده، که آن را از حلقه اصلی خارج می کند. به هر چیزی که بعد از آن می آید فکر کنید await به عنوان جلوگیری از مسدود کردن از طریق پاسخ.

افشای کامل: کد بالا فقط برای نمایش است. در کد واقعی ، من توصیه می کنم fs.readFile، که باعث می شود یک پاسخ به عقب برگردد که می تواند پیرامون یک وعده باشد. هدف کلی هنوز معتبر است ، زیرا این امر باعث می شود I / O از حلقه اصلی جلوگیری کند.

با استفاده از آن بیشتر

اگر به شما بگویم حلقه رویداد بیشتر از پشته تماس و صف پاسخگویی است؟ اگر حلقه رویداد فقط یک حلقه نبود بلکه تعداد زیادی حلقه بود ، چه می شد؟ و اگر می تواند چندین نخ زیر پوشش داشته باشد ، چه می شود؟

حالا ، من می خواهم شما را به پشت نما و نبرد داخلی Node ببرم.

مراحل حلقه رویداد

این مراحل حلقه رویداد است:

مراحل حلقه رویداد

منبع تصویر: اسناد libuv

  1. مهر زمان به روز می شود. حلقه رویداد برای جلوگیری از تماس های مکرر سیستم مربوط به زمان ، زمان فعلی را در ابتدای حلقه ذخیره می کند. این تماس های سیستمی برای libuv داخلی هستند.

  2. آیا حلقه زنده است؟ اگر حلقه دارای دسته های فعال ، درخواست های فعال یا دسته های بسته باشد ، زنده است. همانطور که نشان داده شده است ، تماس های معلق در صف ، حلقه را زنده نگه می دارند.

  3. تایمرهای موعد اجرا می شوند. اینجاست که setTimeout یا setInterval برگشت تماس اجرا می شود. حلقه حافظه پنهان را بررسی می کند اکنون برای بازخورد فعال که منقضی شده باشد اجرا شود.

  4. تماس های معلق در صف اجرا می شوند. اگر تکرار قبلی هرگونه بازگشت را به تأخیر انداخت ، در این مرحله اجرا می شود. نظرسنجی معمولاً بلافاصله تماس های ورودی / خروجی را اجرا می کند ، اما موارد استثنایی وجود دارد. این مرحله با هر گونه اختلاف از تکرار قبلی سرو کار دارد.

  5. کنترل کنندگان بیکار اجرا می شوند – بیشتر از نامگذاری نامناسب ، زیرا این برنامه ها با هر تکرار اجرا می شوند و برای libuv داخلی هستند.

  6. دستگیره ها را برای آماده کنید setImmediate اجرای تماس برگشتی در تکرار حلقه. این دسته ها قبل از بلوک حلقه برای ورودی / خروجی اجرا می شوند و صف را برای این نوع پاسخگویی آماده می کنند.

  7. مهلت زمانی نظرسنجی را محاسبه کنید. این حلقه باید بداند چه مدت برای I / O مسدود می شود. این نحوه محاسبه فاصله زمانی است:

    • اگر حلقه در شرف خروج باشد ، زمان پایان 0 است.
    • اگر دسته یا درخواست فعال وجود نداشته باشد ، زمان پایان 0 است.
    • اگر دسته های بیکاری وجود داشته باشد ، زمان پایان 0 است.
    • اگر هر دسته ای در صف معلق باشد ، مهلت زمانی 0 است.
    • در صورت وجود دسته های بسته ، زمان پایان 0 است.
    • اگر هیچ یک از موارد بالا وجود نداشته باشد ، مهلت زمانی روی نزدیکترین تایمر تنظیم شده است ، یا اگر تایمر فعال وجود ندارد ، بی نهایت.
  8. حلقه برای I / O با مدت زمان فاز قبلی مسدود می شود. تماس های برگشتی I / O در صف در این مرحله اجرا می شود.

  9. بازخوانی دسته را بررسی کنید. این مرحله جایی است setImmediate اجرا می شود ، و همتای تهیه دستگیره ها است. هر setImmediate اجرای مجدد Callback ها در اواسط I / O اجرای Callback در اینجا اجرا می شوند.

  10. بستن تماس مجدد اجرا شود. این دسته های فعال دفع شده از اتصالات بسته هستند.

  11. تکرار به پایان می رسد.

ممکن است تعجب کنید که چرا بلوک های رای گیری برای ورودی و خروج زمانی که قرار است مانع ایجاد نشود؟ حلقه فقط وقتی مسدود می شود که در صف انتظار برگشتی وجود نداشته باشد و پشته تماس خالی باشد. در Node ، نزدیکترین تایمر را می توان توسط تنظیم کرد setTimeout، مثلا. اگر روی بی نهایت تنظیم شود ، حلقه منتظر اتصالات ورودی با کار بیشتر است. این یک حلقه نیمه بی نهایت است ، زیرا نظرسنجی حلقه را زنده نگه می دارد در حالی که دیگر کاری برای انجام کار باقی نمانده و اتصال فعال وجود دارد.

در اینجا نسخه یونیکس این محاسبه وقفه را با تمام شکوه C خود آورده است:

int uv_backend_timeout(const uv_loop_t* loop) {
  if (loop->stop_flag != 0)
    return 0;

  if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
    return 0;

  if (!QUEUE_EMPTY(&loop->idle_handles))
    return 0;

  if (!QUEUE_EMPTY(&loop->pending_queue))
    return 0;

  if (loop->closing_handles)
    return 0;

  return uv__next_timeout(loop);
}

شاید خیلی با C آشنایی نداشته باشید ، اما این مانند انگلیسی خوانده می شود و دقیقاً همان کاری است که در فاز هفت انجام می شود.

نمایش فاز به مرحله

برای نشان دادن هر مرحله در JavaScript ساده:


const http = require('http');



const server = http.createServer((req, res) => {
  
  res.end();
});



server.listen(8000);

const options = {
  
  hostname: '127.0.0.1',
  port: 8000
};

const sendHttpRequest = () => {
  
  
  const req = http.request(options, () => {
    console.log('Response received from the server');

    
    setImmediate(() =>
      
       server.close(() =>
        
        console.log('Closing the server')));
  });
  req.end();
};



setTimeout(() => sendHttpRequest(), 8000);


از آنجا که تماس های ورودی I / O پرونده در فاز چهار و قبل از فاز 9 اجرا می شود ، انتظار دارید setImmediate() اول آتش زدن:

fs.readFile('readme.md', () => {
  setTimeout(() => console.log('File I/O callback via setTimeout()'), 0);
  
  setImmediate(() => console.log('File I/O callback via setImmediate()'));
});

ورودی و خروجی شبکه بدون جستجوی DNS نسبت به ورودی I / O هزینه کمتری دارد ، زیرا در حلقه رویداد اصلی اجرا می شود. پرونده I / O به جای آن از طریق رشته استخر صف می گیرد. یک جستجوی DNS از استخر رشته نیز استفاده می کند ، بنابراین این امر باعث می شود I / O شبکه گرانتر از پرونده I / O باشد.

استخر نخ

گره های داخلی دارای دو قسمت اصلی هستند: موتور جاوا اسکریپت V8 و libuv. پرونده I / O ، جستجوی DNS و I / O شبکه از طریق libuv اتفاق می افتد.

این معماری کلی است:

نمای کلی طراحی استخر نخ

منبع تصویر: اسناد libuv

برای I / O شبکه ، حلقه رویداد در داخل موضوع اصلی نظرسنجی می کند. این رشته ایمن نخ نیست زیرا با یک موضوع دیگر سوئیچ متن را انجام نمی دهد. جستجوی پرونده I / O و DNS مختص پلتفرم است ، بنابراین هدف این است که این موارد را در استخر رشته اجرا کنید. همانطور که در کد بالا نشان داده شده است ، یک ایده این است که خودتان DNS را جستجو کنید تا از استخر نخ خارج شوید. قرار دادن آدرس IP در مقابل localhost، به عنوان مثال ، جستجو را از استخر خارج می کند. استخر نخ دارای تعداد محدودی از نخ های موجود است که می توان از طریق UV_THREADPOOL_SIZE متغیر محیطی. اندازه استخر نخ پیش فرض حدود چهار است.

V8 در یک حلقه جداگانه اجرا می شود ، پشته تماس را تخلیه می کند ، سپس کنترل را به حلقه رویداد می دهد. V8 می تواند از نخ های مختلفی برای جمع آوری زباله خارج از حلقه خود استفاده کند. V8 را به عنوان موتوری تصور کنید که جاوا اسکریپت خام را به خود اختصاص داده و آن را از طریق سخت افزار اجرا می کند.

از نظر یک برنامه نویس معمولی ، جاوا اسکریپت تک رشته ای باقی می ماند زیرا هیچ ایمنی موضوعی وجود ندارد. V8 و libuv داخلی رشته های جداگانه خود را برای رفع نیازهای خود می چرخانند.

اگر مشکلات Node وجود دارد ، با حلقه اصلی رویداد شروع کنید. بررسی کنید که برنامه برای تکمیل یک تکرار چقدر طول می کشد. نباید بیش از صد میلی ثانیه باشد. سپس ، گرسنگی استخر با نخ و موارد خارج شده از استخر را بررسی کنید. همچنین امکان افزایش اندازه استخر از طریق متغیر محیط وجود دارد. آخرین مرحله ، رمزگذاری کد جاوا اسکریپت در V8 است که به طور همزمان اجرا می شود.

بسته بندی کردن

حلقه رویداد با صف گرفتن مجدد تماس ها ، در هر مرحله تکرار می شود. اما در هر مرحله راهی وجود دارد که می تواند نوع دیگری از پاسخ را به صف بکشد.

process.nextTick() در مقابل setImmediate()

در پایان هر مرحله ، حلقه عمل می کند process.nextTick() پاسخگویی توجه داشته باشید که این نوع تماس بخشی از حلقه رویداد نیست زیرا در پایان هر مرحله اجرا می شود. setImmediate() تماس مجدد بخشی از حلقه رویداد کلی است ، بنابراین به همان اندازه که از اسمش فوری است ، نیست. زیرا process.nextTick() به دانش صمیمی حلقه رویداد نیاز دارد ، استفاده از آن را توصیه می کنم setImmediate() به طور کلی

چند دلیل وجود دارد که ممکن است شما به آنها احتیاج داشته باشید process.nextTick():

  1. قبل از ادامه حلقه به I / O شبکه اجازه دهید خطاها ، پاک سازی را انجام دهد یا درخواست را دوباره امتحان کنید.

  2. ممکن است لازم باشد پس از باز شدن پشته تماس اما قبل از ادامه حلقه ، یک پاسخ به تماس را اجرا کنید.

برای مثال بگویید که یک فرستنده رویداد می خواهد یک رویداد را در حالی که هنوز در سازنده خود است ، اخراج کند. قبل از فراخوانی رویداد ، ابتدا پشته تماس باید باز شود.

const EventEmitter = require('events');

class ImpatientEmitter extends EventEmitter {
  constructor() {
    super();

    
    process.nextTick(() => this.emit('event'));
  }
}

const emitter = new ImpatientEmitter();
emitter.on('event', () => console.log('An impatient event occurred!'));

اجازه می دهد تا پشته تماس خاموش شود و از خطاهایی مانند این امر جلوگیری می کند RangeError: Maximum call stack size exceeded. یک گفتار این است که مطمئن شوید process.nextTick() حلقه رویداد را مسدود نمی کند. مسدود کردن می تواند با تماس های برگشتی برگشتی در همان مرحله مشکل ساز شود.

نتیجه

حلقه رویداد در نهایت پیچیدگی ساده است. این یک مشکل سخت مانند ناهمزمانی ، ایمنی موضوعی و همزمانی را می طلبد. آنچه را که کمکی نمی کند یا نیازی به آن نیست ، پاره می کند و حداکثر بهره وری را به م theثرترین شکل ممکن انجام می دهد. به همین دلیل ، برنامه نویسان Node زمان کمتری را برای تعقیب باگ های ناهمزمان و زمان بیشتری را برای ارائه ویژگی های جدید صرف می کنند.