0

An event loop has one or more task queues..

According to what is said later in this specification:

Tasks encapsulate algorithms that are responsible for such work as:

Events: ...

Parsing: ...

Callbacks: ...

Using a resource: ...

Reacting to DOM manipulation: ...

Formally, a task is a struct which has:

Steps: ...

A source: ....

Can I think that every event loop has one or more taskqueues, then author has many operations(Events, Callbacks...), these operations will be put into corresponding task queue.

Like following picture.

enter image description here

Per its source field, each task is defined as coming from a specific task source. For each event loop, every task source must be associated with a specific task queue.

For example, a user agent could have one task queue for mouse and key events (to which the user interaction task source is associated), and another to which all other task sources are associated. Then, using the freedom granted in the initial step of the event loop processing model, it could give keyboard and mouse events preference over other tasks three-quarters of the time, keeping the interface responsive but not starving other task queues. Note that in this setup, the processing model still enforces that the user agent would never process events from any one task source out of order.

enter image description here

OnlyWick
  • 342
  • 2
  • 10
  • I'm not sure if my understanding is correct. : ) – OnlyWick Mar 16 '23 at 02:28
  • @MisterJojo Thank you for pointing that out., `because the event loop processing model grabs the first runnable task from the chosen queue, instead of dequeuing the first task.`. What I am thinking about is whether my task structure is correct. – OnlyWick Mar 16 '23 at 02:32
  • @MisterJojo Maybe my expression is not clear.. Every browser page has an event loop. – OnlyWick Mar 16 '23 at 02:38
  • @MisterJojo I'm curious about whether the task queue of Event loop will have the above structure. `One of the task sources, used to group and serialize related tasks.` – OnlyWick Mar 16 '23 at 02:41
  • ok; Every browser page has an event loop, but every page can't comunicate with the other, so, whch interest ? – Mister Jojo Mar 16 '23 at 02:43
  • @MisterJojo It's not these problems.. I just want to know which queues the task source is put into. e.g. ajax operation will be put into ajax task queue. – OnlyWick Mar 16 '23 at 02:48
  • in any browser there is only one page active at a time, the others are freezed. there is also sanboxing. in any page ther is only one event loop, but an unique event can be proceeds on multiple [sub] elments and that make a set, not a new queue. they are done one by one, and there is no thread. Ajax process are separate and doen't concern event loop – Mister Jojo Mar 16 '23 at 02:55
  • @MisterJojo ..., My bad, I mean ajax's callbacks will be put in **set**. – OnlyWick Mar 16 '23 at 03:02
  • @MisterJojo Use the example of timer(setTimeout), will it be put into corresponding timer set? – OnlyWick Mar 16 '23 at 03:07
  • @OnlyWick I read the comments you guys discussed. I think you maybe has a misunderstanding the queues means.An event loop, like the name, is a loop which means it execute as long as the queue is not empty. But The event loop always will finish all tasks in CURRENT queue, then it turns to execute the microTasks. And after the microTasks are finished, it goes back to Task Queue. But This task queue is not same with the last one, it's a new queue which can be made by many ways, such as execute setTimeout/setInterval/ in your last queue, or your ajax is answerred by server and so on ... – Tony_zha Mar 16 '23 at 03:15
  • @MisterJojo Another question, `instead of dequeuing the first task.`. Don't you need to remove the task in the set after execution? – OnlyWick Mar 16 '23 at 03:16
  • @Tony_zha I know these, I'm curious about the structure of this queue. – OnlyWick Mar 16 '23 at 03:18
  • what ever; this is a stack – Mister Jojo Mar 16 '23 at 03:19
  • @Tony_zha 留个 V. – OnlyWick Mar 16 '23 at 03:19
  • @OnlyWick Vnum: tongningsky – Tony_zha Mar 16 '23 at 03:40
  • https://stackoverflow.com/questions/70900239/is-there-really-a-prioritization-system-for-the-task-queues, https://stackoverflow.com/questions/53627773/which-types-of-queues-are-in-event-loop – Bergi Mar 16 '23 at 04:45
  • @Bergi The tutorials found in our country only show one task queue and one micro task queue, but we don't see that there are multiple task queues. – OnlyWick Mar 16 '23 at 04:53
  • @OnlyWick Well every tutorial should start simple… what were you reading? – Bergi Mar 16 '23 at 05:03
  • @Bergi The articles I read a long time ago are wrong, and now these wrong things have been abandoned by me. – OnlyWick Mar 16 '23 at 05:13
  • If you already know that, then what is your actual question? I'm having trouble to understand what you are asking for. – Bergi Mar 16 '23 at 05:16
  • @Bergi Thank you for the two links. HTML standards only describes some behaviors roughly, but doesn't go deep into concrete implementation. It's really difficult to understand these. I think I just need to understand what's in the specification. – OnlyWick Mar 16 '23 at 05:16
  • @Bergi Actual question is browser's implemention other than concept of Event loop. – OnlyWick Mar 16 '23 at 05:19
  • If you want to ask about a particular browser implementation you should probably adjust the question title – Bergi Mar 16 '23 at 05:49

2 Answers2

1

A task, as defined by the HTML specs is a specs construct. It allows to execute algorithms asynchronously. These algorithm might involve calling some JS script, but that's not a given. Also, all JS isn't executed from a task, for instance all the callbacks in the update the rendering aren't called from a task. So seeing a 1-to-1 relationship between tasks and the JS runtime is often misleading. And this graph you shown seems completely wrong. UI event are sent by the OS to one of the browser's processes, which will communicate to the event-loop's process through IPC and queue the various tasks there. The event-loop will then pick one such task and yet again delegate to various other processes and sometime wait for these to do their job, like it does for the process where the JS runtime is ran.

As per your quote, the HTML specs do not require that there are multiple event-queues. What they do require however, is that there are multiple task-sources. This is to ensure that two tasks, queued on the same task-source will execute in the queued order.

During the event-loop processing, the browser will start by choosing one task-source from which it will execute the oldest available task.
This is critical since this allows to have a prioritization system among all the task-sources. This is what your quote is talking about when they say that there could be one event-queue for UI events, and one for all the rest, because it is generally preferable to treat UI events as being "user-visible", since they triggered the event they expect a fast visible response to it. Whereas, e.g. for network tasks, a lot of other actors were involved, having a small delay is generally more acceptable.

And indeed, most implementations actually have at least one event-queue dedicated for just the UI task-source. But they generally don't have a single task-queue for all the other task-sources. This would mean that a network task queued before a message task would be executed before. Most browsers will have yet another task-queue just for network tasks, and another for timer tasks, and some could even have one per MessagePort instance, since each MessagePort has its own task-source. Firefox even has one event-queue for timers that are scheduled during the page load, and another queue for the ones scheduled afterwards, so that the former can have a lower priority (and match the old Chrome's behavior with their 1ms minimal timeout).

In the near future I even expect this wording about the possibility to have a single event-queue to be removed, because we'll soon have access to the Prioritized Task Scheduling API (it already there in Chrome), which does require multiple task-queues since the priority is enforced there.

So, to recap, yes, you can see the various event-queues as being ordered lists of task to be executed, and yes different kinds of task may be queued in different queues. But unless you're using the Prioritized Task Scheduling API, you shouldn't expect any particular behavior as this is currently all let to implementors to decide which behavior they deem the best to their users.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
-2

The idea of a queue is just conceptual to allow humans to describe the architecture in a way familiar to ordinary people. In code, there is no actual "queue". Because the tasks don't actually queue, they're listed.

In code what you actually have is a task group or set or collection (implemented as an array of tasks or a linked list of tasks.. basically a database of tasks). What actually do the queuing are the events.

The basic program flow is actually fairly simple to understand. The pseudocode looks something like this:

while(still_waiting_for(events)) {

  execute_script()

  current_event = wait_for(events)
  current_task = search_related_task(tasks, current_event)
  call_function(current_task.callback)

}

In the code above, there's no variable or data structure that represents the event queue. Instead we just have a variable that stores the list of all events we're listening to/waiting for. The actual event "queue" is normally managed by the OS and is invisible in our code.

The tasks variable is all the tasks that are waiting for their respective events. Like I previously mention above, this is normally a set or collection of tasks. We call this the task queue but as you can see the tasks are not queuing (lining up) for the events. Instead when an event happens we search the for the corresponding task in tasks.

Now, the data structure for different types of tasks may be slightly different (for example, timer tasks are usually handled very differently). So because C/C++ is a typed language one valid way to handle this fact is to have multiple task lists (task queues). Another reason might be that you may want certain type of tasks to be evaluated before waiting for any event. The obvious one being callbacks to setImmediate(). In which case you have a separate task queue for setImmediate callbacks to be processed before everything else:

while(still_waiting_for(events)) {

  execute_script()

  for (i=0; i<immediate_tasks.length; i++) {
    immediate = immediate_tasks[i];
    call_function(immediate.callback)
  }

  current_event = wait_for(events)
  current_task = search_related_task(tasks, current_event)
  call_function(current_task.callback)

}

The actual coding to implement this in C/C++ may be a bit confusing to a typical C programmer but should be fairly easy to understand for someone used to thinking asynchronously in javascript. The low level function for handling asynchronous events vary depending on what OS you're using (eg, poll/epoll on Linux, overlapped I/O on Windows, kqueue for BSD/Mac OS) and different javascript interpreters handle them in different ways (eg, libuv in Node.js which itself uses epoll or kqueue or overlapped I/O depending on which OS you compile Node.js on) but the basic way they work are similar.

I'm going to use the cross-platform (POSIX) select() function to demonstrate how the wait_for(events) part actually work in real code:

while(1) {
  retval = select(maxNumberOfIO, readableIOList, writableIOList, NULL, timeout);

  if (retval == -1) {
    perror("Invalid select()");
  }
  else if (retval) {
    //  Find which file descriptor triggered the event
    for (i=0; i<maxNumberOfIO; i++) {
      if (FD_ISSET(i, readableIOList) && readable_task_queue[i]) {
        call_js_callback(readable_task_queue[i]);
      }
      if (FD_ISSET(i, writableIOList) && writable_task_queue[i]) {
        call_js_callback(writable_task_queue[i]);
      }
    }
  }
}

For the select() system call, if you pass in 0 as the value of timeout it will wait forever for an event to happen. This allows you to set a time limit for waiting in case you need to execute other code (eg. waiting for keyboard or mouse event which may not use the select() system call to communicate with your process). This timeout also allows us a simple way to implement timer events like setTimeout and setInterval:

while(1) {
  // Calculate value of timeout:
  nearest_timer = find_nearest_timer(timer_task_queue);
  
  gettimeofday(&current_time, NULL);

  now_millisecs = current_time.tv_sec*1000 + current_time.tv_usec/1000;

  next_timeout_millisecs = nearest_timer.timeout - now_millisecs;
  if (next_timeout_millisecs < 0) {
    next_timeout_millisecs = 1; // remember, zero means wait forever
  }

  timeout.time_t = next_timeout_millisecs/1000;
  timeout.suseconds_t = (next_timeout_millisecs%1000)*1000;

  // Wait for events:
  retval = select(maxNumberOfIO, readableIOList, writableIOList, NULL, timeout);

  // Process event:
  if (retval == -1) {
    perror("Invalid select()");
  }
  else if (retval) {
    //  Find which file descriptor triggered the event
    for (i=0; i<maxNumberOfIO; i++) {
      if (FD_ISSET(i, readableIOList) && readable_task_queue[i]) {
        call_js_callback(readable_task_queue[i]);
      }
      if (FD_ISSET(i, writableIOList) && writable_task_queue[i]) {
        call_js_callback(writable_task_queue[i]);
      }
    }
  }
  else {
    // If we reach here it means we've timed out.
    // So call the timer callback:

    call_js_callback(nearest_timer);
  }
}

As you can see, because timers work differently from normal I/O events we use a separate timer event queue.

To get better familiarity with the logic flow you can try implementing a simple single-threaded server in C/C++ yourself using the select() system call. I've done it several times. The first time as a homework assignment in college and one time when I was tasked with adding asynchronous I/O to the Ferite programming language.

slebetman
  • 109,858
  • 19
  • 140
  • 171
  • The term "task" in the html spec refers to a handler that is executed (or: becomes ready to execute) when an even occurs. Your answer however uses the meaning where a "task" is something ongoing that emits the events (like completion). This makes it a bit confusing. – Bergi Mar 16 '23 at 05:10
  • Are you certain that JS runtimes do not continue polling the OS while executing the callbacks in parallel? Sure, it can be done on only a single thread in lockstep, without an actual queue, but using one (or multiple) queues seems like a natural extension for a multithreaded implementation. – Bergi Mar 16 '23 at 05:14
  • @Bergi I'm not sure how you're reading the code but it's obvious that the thing that's ongoing is the OS itself and the thing that's waiting is the `select()` function. I'm using tasks to mean a data structure that stores the callback (eg. `task.callback` or `call_js_function(task)`) which is how it's defined in the spec. – slebetman Mar 16 '23 at 05:23
  • As for how js runtimes implement the wait, it depends. In Node.js they use the OS async I/O feature (eg. `select()`) to wait for network events but they use a thread pool to manage disk event. It's not impossible to use async I/O programming for disk events (as is done in the Tcl programming language) but is kind of messy when it comes to doing it on Windows. Thus it's simpler to do traditional blocking I/O and then use IPC to asynchronously update the main thread. – slebetman Mar 16 '23 at 05:26
  • @Bergi Basically Node.js uses pipes as the asynchronous primitive for managing disk I/O which internally they handle using blocking reads and writes in threads. – slebetman Mar 16 '23 at 05:28