Event Loop در دارت

حتمن توی دارت با async ها کار کردی. یه Future استفاده میکنی و یه await میگذاری پشتش و یه مدت بعد کد بعد await اجرا میشه.

ولی داستانش چیه و چطوری توسط دارت هندل میشه؟

دارت برای هندل کردن این موضوع یه چیزی به اسم Event Loop داره!!!

ولی Event Loop چیه و چه وظیفه ای داره؟

قبل از اینکه بفهمیم وظیفه ی Event loop چیه باید با مفهومی به اسم Event Queue آشنا بشیم. Event Queue همون طور که از اسمش مشخصه یه Queue یا یه صف هست و همون طور که بازم از اسمش مشخصه یه صفی از event هاست.

اما Queue چیه دیگه؟

Queue یه دیتا استراکچر هست مثل List که میتونیم داخلش یه سری دیتا بریزیم. مثل وقتی که یه لیستی از int ها داری (List<int>) و داخلش یه سری integer میریزی, میتونی یه Queue از int ها داشته باشی. ولی تفاوت Queue با List این هست که توی Queue ایتم ها از ته وارد میشن و از سرش خارج میشن و هر ایتمی که زودتر وارد بشه, زودتر هم خارج میشه. دقیقن مثل صف نونوایی که هر کی زودتر واردش بشه, زودتر هم نون میگیره. Queue ها هم دقیقن همین شکلی هستن و ایتم ها همین طوری دل بخواهی نمیتونن ازش خارج بشن, بلکه توی یه صف قرار میگیرن و با همون ترتیبی که وارد شده بودن خارج میشن.

کد زیر متد های یه Queue رو نشون میده:

abstract class Queue<T>{

  bool get isEmpty;
  bool get isFull;
  int get size;

  void enqueue(T item);

  T dequeue();

  T peek();
}

برای وارد کردن یه ایتم جدید به queue یا صف از متد enqueue استفاده میشه و برای دراوردن ایتمی که سر صف ایستاده هم از متد dequeue استفاده میشه.

برای مثال اگه بگم :

enqueue(1)

enqueue(5)

enqueue(4)

یه صف یه این شکل شکل میگیره (ار چپ به راست)


1

5…1

4…5…1

حالا اگه متد Dequeue رو صدا بزنم 1 از صف خارج میشه و صف به شکل زیر در میاد:

4…5

حالا اگه دوباره enqueue رو صدا بزنم و بگم:

enqueue(9)

صف به شکل زیر میشه:

9…4…5

حالا اگه dequeue رو کال کنم 5 خارج میشه و صف به شکل زیر میشه:

9….4

همین طور queue رو میتونی به شکل یه لوله در نظر بگیری که ایتم ها از اینورش میرن تو و از اونروش در میان و هر کی زودتر بره تو زودتر هم میاد بیرون. پس اونایی که زودتر رفتن تو اولویت بالاتری برای خارج شدن دارن.

حالا بریم سر ادامه ی داستان اصلی!!!

گفتیم که یه چیزی به اسم Event Queue داریم, یعنی صفی از Event ها (اینکه Event چیه رو بهش میرسیم, فقط اینجا در نظر بگیر که یه صفی از Event ها داریم!!!)

حالا وظیفه ی Event Loop اینه که توی این صف بچرخه و دونه دونه ایتم ها رو ازش بکشه بیرون و اجرا کنه. یعنی دونه دونه Event ها رو از صف بکشه بیرون و اجراشون کنه و این کار رو تا وقتی که Event Queue خالی بشه ادامه بده. تصویر زیر این داستان رو بیان میکنه:

همون طور که میبینی یه Event Queue داریم که Event ها از اون طرفش یعنی سمت چپش واردش میشه و سمت راستش هم یه Event Loop ایستاده و دونه دونه Event ها رو میگیره و میکشه بیرون و اجراشون میکنه!!!

خب که چی؟

حالا میرسیم به اصل داستان.

گفتم که دارت برای هندل کردن async ها از Event Loop استفاده میکنه. خب این یعنی چی؟

الان بهت میگم ….

برای مثال وقتی که یه تایمر رجیستر میکنی (با کلاس Timer) و بهش میگی 50 ثانیه دیگه فلان کار رو انجام بده, بعد از 50 ثانیه یه Event به Event Queue اضافه میشه و بعدش Event Loop اون Event رو از صف بیرون میکشه و اون کاری رو که تو بهش گفته بودی رو انجا میده.

یا وقتی که کاربر روی یه باتن کلیک میکنه یه Event به Event Queue اضافه میشه و بقیه داستان.

یا وقتی که یه Request به سمت سرور میفرستی و پشتش await میگذاری و میگی تا Response اومد فلان کار رو بکن هم همین اتفاق میوفته. به این صورت که وقتی که رسپانس اومد یه Event به Event Queue اضافه میشه (یعنی از تهش وارد میشه) و بعد Event Loop دستشو میکنه توی Event Queue (از سرش) و Event رو بیرون میاره و اجراش میکنه!!!

پ.ن: البته داستان به همین سادگی نیست و یه من یه کوچولو سادش کردم و علما ایراد نگیرن!!!

برای جا افتادن مطلب تصویر زیر رو ببین:

اول یوزر یه کاراکتر رو تایپ کرده, برای مثال دستشو گذاشته روی دکمه ی A. پس یه Event جدید (key) رفته توی Queue و سر صف قرار گرفته.

بعدش یوزر روی یه چیزی کلیک کرده و click event رفته توی queue.

بعدش یه تایمری که قبلن رجیستر شده وقتش رسیده و یه timer event رفته توی queue.

و…

و Event Loop هم اینارو به ترتیبی که وارد شدن بیرون میکشه و اجرا میکنه.

برای مثال اول حرف A تایپ میشه و نشون داده میشه (یا کالبک متناظر با دکمه ی حرف A کال میشه). بعدش کالبک مربوط به اون چیزی که یوزر روش کلیک کرده کال میشه و بعدش هم کالبک تایمر و … .

تصویر زیر میتونه گفته های بالا رو به صورت واضح بیان کنه:

همون طور که میبینی اول اول متد main اجرا میشه و تا ته میره (حواست باشه تا ته متد main اجرا میشه.) بعدش Event Loop وارد داستان میشه و تا وقتی که Event ای توی Queue وجود داشته باشه اون ها رو بیرون میکشه و handler هاشون رو اجرا میکنه. وقتی که Queue خالی باشه و هیج event ای هم در حالت pending نباشه, برای مثال هیج تایمری منتظر رسیدن وقتش نباشه, برنامه تموم میشه و به اصطلاح exit میشه.

چرا اپ های فلاتر هیچ موقع exit نمیشن؟

چون که همیشه منتظر یه click event یا key event از سمت کاربر هستن.

ولی به چی میگیم Event؟

به هر چیزی که از خارج از اپ یا خارج از خود محیط دارت بیاد و خود دارت توش نقشی نداشته باشه میگیم event . در مقابل این event ها یه چیزی به اسم microtask داریم که توسط خود دارت (داخل کد) برنامه ریزی میشن و از این رو میگیم که event ها از بیرون میان و microtask ها از داخل خود کد دارت برنامه ریزی میشن. (در ادامه به microtask ها میرسیم)

برای مثال:

  • رویداد کلیک کردن (از بیرون داره میاد و باید کاربر کلیک کنه تا این event بیاد)
  • خونده شدن یه فایل از حافظه
  • اومدن رسپانس از سرور

Microtask چیه؟

علاوه بر event ها یه چیزی به اسم microtask و همچینن یه چیزی به اسم microtask queue داریم. پس شد 2 تا :

  • Event و Event Queue
  • Microtask و Microtask Queue

اینا هر کدوم برای خودشون یه صف جداگونه دارن و event ها میرن توی event queue و microtask ها میرن توی اون یکی صف.

ولی چه جوری میفهمن که برن توی کدوم صف؟

گفتم که event ها همگی از بیرون میان مثل کلیک کاربر. ولی در مقابل توی دارت یه متدی یا درواقع یه فانکشنی به اسم ScheduleMicrotask داریم که هر موقع کالش کنیم و بهش یه کالبک بدیم, میتونیم یه microtask رجیستر کنیم و بفرستیمش توی اون یکی صف!!!

scheduleMicrotask(() {
    print("something...");
  });

حالا کار event loop سختر میشه و علاوه بر اینکه باید event ها رو از event loop بیرون بکشه و اجرا کنه, باید microtask ها رو هم از microtask queue بیرون بکشه و اجرا کنه.

Event Loop چطوری این کار رو میکنه؟

به تصویر زیر دقت کن:

اول متد main اجرا میشه. و بعدش دونه دونه دونه دونه micro task ها از micro task queue بیرون کشیده میشن و اجرا میشن و هر موقع که micro task queue خالی خالی خالی شد, میره سر وقت event queue و دونه دونه event ها رو بیرون میکشه و event handler هاشون رو اجرا میکنه. پس micro ها به event ها اولویت دارن و تا صفشون خالی نشه به هیچ event ای رسیدگی نمیشه و event handler اش اجرا نمیشه.

یه نکته ی خیلی ظریفی اینجا وجود داره که اگه وسط event handler یکی از این event ها, برای مثال توی کالبک یه باتن, یه microtask برنامه ریزی بشه(با فانکشن scheduleMicrotask), مجددن صف microtask ها پر میشه و باید اول به وضعیت microtask ها رسیدگی بشه و بعدش دوباره به بقیه event ها پرداخته بشه. از این روی خطی که توی شکل زیر مشخصش کردم به خاطر همین هست.

نکته ی مهمی که وجود داره این هست که دارت Single Thread هست و همه این ها رو توی یه ترد انجام میده. یعنی اول متد main از اول تا اخط اخر اجرا میشه و بعد event loop وارد عمل میشه و هر کذوم از از این microtask handler ها یا event handler ها به ترتیب و توی یه ترد اجرا میشن.

نکته ای که از این ماجرا در میاد اینه که برای مثال یه تایمر برای 30 ثانیه ی دیگه رجیستر میکنی ولی دقیقن سر 30 ثانیه اجرا نمیشه و مثلن چند صدم ثانیه delay داره. چرا؟ چون ممکنه چنتا event دیگه قبل تایمر باشن و تا میاد timer handler ران بشه یکم طول بکشه (single thread)

مثال

کد زیر رو با هم اجرا کنیم و بینم نتیجه چی میشه:

اول خودت ترتیب اجرا رو روی کاغذ بنویس و بعد جواب رو ببین تا برات تمرین بشه!!!

void main(List<String> arguments) {
  
  print('main #1 of 2');
  scheduleMicrotask(() => print('microtask #1 of 2'));

  Future.delayed(Duration(seconds:1),
                     () => print('future #1 (delayed)'));
  Future(() => print('future #2 of 3'));
  Future(() => print('future #3 of 3'));

  scheduleMicrotask(() => print('microtask #2 of 2'));

  print('main #2 of 2');
}

نکته 1)

همون طور که بارها تاکید کردم و میدونم گوش نکردی اول کار متد main از اول تا اخر اجرا میشه و هیج کدوم از hanler ها اجرا نمیشن پس اول این دو خط پرینت میشن:

main #1 of 2

main #2 of 2

نکته 2)

حالا میریم توی سورس کد دارت و Future رو بررسی میکنیم. اول متد Future(() => print(‘future #2 of 3’)) :

میبینی که یه تایمر با delay یا تاخیر 0 ثانیه اجرا میکنه و بلافاصله باید اجرا شه. ولی تایمر ها همون طور که میدونیم یه event به event loop اضافه میکنن.

چرا؟

در واقع وقتی تایمر رجیستر میکنی داری به سیستم عامل میگی که مثلن 30 ثانیه دیگه هم خبر بده و وقتی که خبرش اومد یه چیزی از بیرون داره میاد و میشه event.

خب بریم سراغ Future.delayed(Duration(seconds:1):

خب اینم یه تایمر زمان دار رحیستر میکنه.

پس بعد از اجرا شدن main باید مایکرو ها اجرا بشن, چون که به event هایی که از تایمر ها (فیوچر ها) میان اولویت دارن. و بعدش هم دو تا فیوچری که delay نداشتن به ترتی اجرا میشن و در نهایت فیوچر delay دار.

مثال)

void main(List<String> arguments) {
  
  print('main #1 of 2');
  scheduleMicrotask(() => print('microtask #1 of 3'));

  Future.delayed(Duration(seconds:1),
      () => print('future #1 (delayed)'));

  Future(() => print('future #2 of 4'))
      .then((_) => print('future #2a'))
      .then((_) {
        print('future #2b');
        scheduleMicrotask(() => print('microtask #0 (from future #2b)'));
      })
      .then((_) => print('future #2c'));

  scheduleMicrotask(() => print('microtask #2 of 3'));

  Future(() => print('future #3 of 4'))
      .then((_) => Future(
                   () => print('future #3a (a new future)')))
      .then((_) => print('future #3b'));

  Future(() => print('future #4 of 4'));
  scheduleMicrotask(() => print('microtask #3 of 3'));
  print('main #2 of 2');
}

خب اول که main از خط اول تا اخر اجرا میشه:

main #1 of 2
main #2 of 2

بعدش event loop وارد میشه و چون توی صف مایرکو ها 3 تا مایکرو رجیستر شدن, پس از اونا شروع میکنه و اولش 3 تا micro handler اجرا میشن:

microtask #1 of 3

microtask #2 of 3

microtask #3 of 3

بعد از اون که مایکرو ها تموم شدن میره سروقت صف event ها و اولین فیوچری که delay نداره سر صف ایستاده (فیوچر شماره 2) و بنابراین handler اش اجرا میشه:

new Future(() => print('future #2 of 4'))
      .then((_) => print('future #2a'))
      .then((_) {
        print('future #2b');
        scheduleMicrotask(() => print('microtask #0 (from future #2b)'));
      })
      .then((_) => print('future #2c'));

future #2 of 4

نکته 1)

پشت سر این فیوچر 3 تا then خورده و باید دونه دونه اجرا بشن.

future #2a

future #2b

future #2c

ولی وسط هندلر دومی یه مایکرو رجیستر میشه میره توی صف مایکرو ها و بنابراین دوباره صف مایکرو ها پر میشه و چون مایکرو ها به event ها اولویت دارن, پس باید زودتر اجرا بشه. (همون خط قرمزی که روی شکل کشیدم رو یادت بیاد)

microtask #0 (from future #2b)

پس دوباره microtask queue خالی میشه و باید بریم سر وقت event queue.

مرحله ی بعدی نوبت میرسه به دومین فیوچری که delay نداره یعنی شماره 3:

Future(() => print('future #3 of 4'))
      .then((_) => Future(
                   () => print('future #3a (a new future)')))
      .then((_) => print('future #3b'));

اول کالبک خودش اجرا میشه (از event queue)

future #3 of 4

خود این فیوچر 2 تا then داره:

then اولی اجرا میشه, ولی داخلش یه دونه فیوچر جدید درست میشه و چون که یه تایمر با delay صفر رو رجیستر میکنه میره ته صف event ها و بناراین باید معطل بمونه تا بقیه event هایی که قبلن توی صف بودن خالی شن.

پس توی نوبت بعدی فیوچر شماره 4 که قبلن توی صف بوده از صف میاد بیرون:

Future(() => print('future #4 of 4'));

Future(() => print(‘future #4 of 4’));

بعد از اون دوباره نوبت میرسه به فیوچری که داخل then فیوچر شماره 3 قرار گرفته بود:

Future(() => print('future #3 of 4'))
      .then((_) => Future(
                   () => print('future #3a (a new future)')))
      .then((_) => print('future #3b'));

future #3a (a new future)

بعد از اون و خاتمه ی then اول نوت به اجرای then دوم میرسه :

future #3b

و در نهایت فیوچر delay دار اجرا میشه:

future #1 (delayed)

دیدگاهتان را بنویسید

error: Alert: Content is protected !!