-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
doc: add topic - event loop, timers, nextTick()
#4936
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
46f0702
5a28415
dc1b8a5
bb5b682
ba98380
936bf17
35cf726
f80d7cc
254694b
45fb2fe
d6d76f5
c133caf
f425164
1bd3e6c
7574d4b
8dc6ecb
d82a7f1
1dc26f6
82d0fb8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,23 @@ | ||
| # Overview of the Event Loop, Timers, and `process.nextTick()` | ||
| # The Node.js Event Loop, Timers, and `process.nextTick()` | ||
|
|
||
| ## What is the Event Loop? | ||
|
|
||
| The event loop is what allows Node.js to perform non-blocking I/O | ||
| operations — despite the fact that JavaScript is single-threaded — by | ||
| offloading operations to the system kernel whenever possible. | ||
|
|
||
| Since most modern kernels are multi-threaded, they can handle | ||
| multiple operations executing in the background. When one of these | ||
| operations completes, the kernel tells Node.js so that the appropriate callback | ||
| may added to the `poll` queue to eventually be executed. We'll explain | ||
| this in further detail later in this topic. | ||
|
|
||
| ## Event Loop Explained | ||
|
|
||
| When Node.js starts, it initializes the event loop, processes the provided | ||
| input script (or drops into the REPL, which is not covered in this document) | ||
| which may make async API calls, schedule timers, or call `process.nextTick()`, | ||
| then begins processing the event loop. | ||
|
|
||
| The Following diagram shows a simplified overview of the event loop's | ||
| order of operations. | ||
|
|
@@ -18,54 +37,146 @@ order of operations. | |
|
|
||
| *note: each box will be referred to as a "phase" of the event loop.* | ||
|
|
||
| *There is a slight discrepancy between the Windows and the Unix/Linux | ||
| implementation, but that's not important for this demonstration. The most | ||
| important parts are here. There are actually seven or eight steps, but the | ||
| ones we care about — ones that Node.js actually uses are these four.* | ||
| Each phase has a FIFO queue of callbacks to execute. While each phase is | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. double space after |
||
| special in its own way, generally, when the event loop enters a given phase, | ||
| it will perform any operations specific to that phase, then execute callbacks | ||
| in that phase's queue until the queue has been exhausted or the maximum number | ||
| of callbacks have executed. When the queue has been exhausted or the callback | ||
| limit is reached, the event loop will move to the next phase, and so on. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like there are many of these mid-sentence double spaces, please clean them up 😉
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm. I suspect that's due to my auto-wrap settings for sublime text.. Thank you for bringing this to my attention! |
||
|
|
||
| Since any of these operations may schedule _more_ operations and new events | ||
| processed in the `poll` phase are queued by the kernel, poll events can be | ||
| queued while polling events are being processed. As a result, long running | ||
| callbacks can allow the poll phase to run much longer than a timer's | ||
| threshold. See the [`timers`](#timers) and [`poll`](#poll) sections for more | ||
| details. | ||
|
|
||
| _**NOTE:** There is a slight discrepancy between the Windows and the | ||
| Unix/Linux implementation, but that's not important for this demonstration. | ||
| The most important parts are here. There are actually seven or eight steps, | ||
| but the ones we care about — ones that Node.js actually uses are these four._ | ||
|
|
||
|
|
||
| ## Phases Overview: | ||
|
|
||
| * `timers`: this phase executes callbacks scheduled by `setTimeout()` | ||
| and `setInterval()`. | ||
| * `pending callbacks`: this phase executes callbacks for specific types of TCP | ||
| errors, for example. | ||
| * `poll`: this is the phase in which the event loop either processes | ||
| its queue of callbacks, or sits and waits for new callbacks for incoming | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. poll phase doesn't do anything initially on its own. (cc @bnoordhuis) It will sit and wait to be notified on a file descriptor. At which time libuv will call the corresponding callback. |
||
| connections, completion of async file I/O, DNS operations, etc. to be added | ||
| to the queue. Ideally, most scripts spend most of their time here. If not, | ||
| it might be time consider refactoring or scaling to more processes. | ||
| * `setImmediate`: This phase allows a person to execute callbacks | ||
| immediately after the `poll` phase has completed. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possibly make a note that when a setImmediate is schedules, the poll phase will still run and check for any waiting notifications. Which will then be processed. If not then execution will proceed immediately to calling the setImmediate. |
||
|
|
||
| ## Phases in Detail | ||
|
|
||
| ### timers | ||
|
|
||
| Contrary to what a person might expect, a timer specifies the **threshold** | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. node behaviour is what any software developer not on a high-end real-time OS/hardware would expect. Nothing happens at exact time in a computer, its all aproximate. You get either before or after semantics, and after is what anyone would expect. I think this text makes it sound like node is doing something odd here, when its how every commodity OS works.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well to be fair @sam-github, people ask a lot about why their timers and timings aren't accurate. It might be worth mentioning that it's not node-specific though:
Or something like that.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lets not quibble about the thoughfulness of peoples expectations, then :-), but just say "Timers callbacks will run as early as they can be scheduled after the specified amount of time has passed. Operating System scheduling or the running of other callbacks may delay them."
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sounds good to me! Why be presumptuous?
I'm going to go with this ^^ |
||
| _after which_ the provided callback _may be executed_ rather than the | ||
| **exact** time a person _wants it to be executed_. | ||
|
|
||
| _**Note**: Technically, the [`poll` phase](#poll) controls when timers are | ||
| executed. While waiting for the minimum threshold specified when scheduling | ||
| the timer, the `poll` phase will iterate through its queue of callbacks as | ||
| usual. Collectively, these callbacks **may or may not** take longer to | ||
| complete than the minimum threshold specified by the timer. The actual amount | ||
| of time depends on how many callbacks end up being executed and how long they | ||
| take._ | ||
|
|
||
| For example, say you schedule a timeout to execute after a 100 ms threshold, | ||
| then your script starts asynchronously reading a file which takes 95 ms: | ||
|
|
||
| ```js | ||
|
|
||
| var fs = require('fs'); | ||
|
|
||
| function someAsyncOperation (callback) { | ||
|
|
||
| // let's assume this takes 95ms to complete | ||
| fs.readFile('/path/to/file', callback); | ||
|
|
||
| } | ||
|
|
||
| var timeoutScheduled = Date.now(); | ||
|
|
||
| ## timers | ||
| setTimeout(function () { | ||
|
|
||
| This phase executes callbacks scheduled by `setTimeout()` and `setInterval()`. | ||
| var delay = Date.now() - timeoutScheduled; | ||
|
|
||
| When you create a timer, you make a call to `setTimeout()`. Then, when | ||
| the poll phase of the event loop is entered, the number of ms before the | ||
| soonest timer is to be called is set as the poll's timeout. Meaning the | ||
| poll phase will return after "timeout" ms. After that many | ||
| milliseconds, the `poll` phase will return and wrap back around to the | ||
| timers phase where those callbacks can be processed. | ||
| console.log(delay + "ms have passed since I was scheduled"); | ||
| }, 100); | ||
|
|
||
| Take note that the poll phase can only return while idle; execution of | ||
| a callback is allowed to run to completion, and can cause unexpected | ||
| delay running the timer. | ||
|
|
||
| *Note: The `poll` phase technically controls when timers are called due to its | ||
| ability to cause a thread to sit idly without burning CPU in order to stall the | ||
| event loop so the timer can execute.* | ||
| // do someAsyncOperation which takes 95 ms to complete | ||
| someAsyncOperation(function () { | ||
|
|
||
| ## pending callbacks: | ||
| var startCallback = Date.now(); | ||
|
|
||
| This phase executes callbacks for specific types of TCP errors, for example. | ||
| // do something that will take 10ms... | ||
| while (Date.now() - startCallback < 10) { | ||
| ; // do nothing | ||
| } | ||
|
|
||
| ## poll: | ||
| }); | ||
| ``` | ||
|
|
||
| When the event loop enters the `poll` phase, it has an empty queue | ||
| (`fs.readFile()` has not completed) so it will wait for the number of ms | ||
| remaining until the soonest timer's threshold is reached. While it is | ||
| waiting 95 ms pass, `fs.readFile()` finishes reading the file and its | ||
| callback which takes 10 ms to complete is added to the `poll` queue and | ||
| executed. When the callback finishes, there are no more callbacks in the | ||
| queue, so the event loop will see that the threshold of the soonest timer has | ||
| been reached then wrap back to the `timers` phase to execute the timer's | ||
| callback. In this example, you will see that the total delay between | ||
| the timer being scheduled and its callback being executed will be | ||
| 105ms. | ||
|
|
||
| Note: To prevent the `poll` phase from starving the event loop, libuv also has | ||
| a hard maximum (system dependent) before it stops poll'ing for more events. | ||
|
|
||
| ### pending callbacks: | ||
|
|
||
| This phase executes callbacks for some system operations such as types of TCP | ||
| errors. For example if a TCP socket receives `ECONNREFUSED` when | ||
| attempting to connect, some *nix systems want to wait to report the | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might need to escape the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. indeed, thank you. |
||
| error. This will be queued to execute in the `pending callbacks` phase. | ||
|
|
||
| This is the phase in which the event loop sits and waits for incoming | ||
| connections to be received. Ideally, most scripts spend most of their time | ||
| here. | ||
| ### poll: | ||
|
|
||
| ## `setImmediate()`: | ||
| When the event loop enters the `poll` phase _and there are no timers | ||
| scheduled_, one of two things will happend: | ||
|
|
||
| * _if the `poll` queue **is not empty**_, the event loop will iterate through | ||
| its queue of callbacks executing them synchronously until either the queue has | ||
| been exhausted, or the system-dependent hard limit is reached. | ||
|
|
||
| * _if the `poll` queue is **empty**_, the event loop will wait for a | ||
| callback | ||
| to be added to the queue, then execute it immediately. | ||
|
|
||
| Once the `poll` queue is empty the event loop will check for timers _whose | ||
| time thresholds have been reached_. If one or more timers are ready, the | ||
| event loop will wrap back to the timers phase to execute those timers' | ||
| callbacks. | ||
|
|
||
| ### `setImmediate()`: | ||
|
|
||
| `setImmediate()` is actually a special timer that runs in a separate phase of | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure that I would call it a timer really since it has no "timeout" value per se ...
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just used @trevnorris' words from the presentation link in the OP, but I am open so suggestions as per his approval.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's located in https://nodejs.org/api/timers.html :)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. its a timer? really? that surprises me, its behaviour seems unrelated to time.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TBH, I do not have enough intimate knowledge of the codebase to argue for or against whether or not it is a timer. I just said its a special form of timer because that's what I got from the presentation which was the basis. I'm hoping to get this landed ASAP, so that EVERYONE can discuss and refine this doc
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| the event loop. It uses a libuv API that schedules callbacks to execute after | ||
| the poll phase has completed. | ||
| the `poll` phase has completed. | ||
|
|
||
| Generally, as the code is executed, the event loop will eventually hit the | ||
| `poll` phase where it will wait for an incoming connection, request, etc. | ||
| However, after a callback has been scheduled with `setImmediate()`, at the | ||
| start of the poll phase, a check will be run to see if there are any callbacks | ||
| waiting. If there are none waiting, the poll phase will end and continue to | ||
| the `setImmediate` callback phase. | ||
| However, after a callback has been scheduled with `setImmediate()`, | ||
| then the `poll` phase becomes idle, it will end and continue to the | ||
| `setImmediate` phase rather than waiting for `poll` events. | ||
|
|
||
| ### `setImmediate()` vs `setTimeout()` | ||
| ## `setImmediate()` vs `setTimeout()` | ||
|
|
||
| How quickly a `setImmediate()` callback is executed is only limited by how | ||
| quickly the event loop can be processed whereas a timer won't fire until the | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. until "at least" the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
|
|
@@ -83,14 +194,15 @@ microsecond scale (1 ms = 1000 µs.) | |
|
|
||
| You may have noticed that `process.nextTick()` was not displayed in the | ||
| diagram, even though its a part of the asynchronous API. This is because | ||
| `process.nextTick()` is not technically part of the event loop. Instead, it | ||
| is executed at the end of each phase of the event loop. | ||
| `process.nextTick()` is not technically part of the event loop. Instead, the | ||
| nextTickQueue will be processed after the current operation completes, | ||
| regardless of the current `phase` of the event loop. | ||
|
|
||
| Looking back at our diagram, any time you call `process.nextTick()` in a given | ||
| phase, all callbacks passed to `process.nextTick()` will be resolved before | ||
| the event loop continues. This can create some bad situations because **it | ||
| allows you to "starve" your I/O by making recursive `process.nextTick()` | ||
| calls.** which prevents the event loop from reaching the poll phase. | ||
| calls.** which prevents the event loop from reaching the `poll` phase. | ||
|
|
||
| ### Why would that be allowed? | ||
|
|
||
|
|
@@ -112,10 +224,13 @@ arguments to `process.nextTick()` allowing it to take any arguments passed | |
| after the callback to be propagated as the arguments to the callback so you | ||
| don't have to nest functions. | ||
|
|
||
| What we're doing is passing an error back to the user. As far as the _event | ||
| loop_ is concerned, this happens **synchronously**. However, as far as the | ||
| _user_ is concerned, it occurs **asynchronously**: `apiCall()` always runs its | ||
| callback *after* the rest of the user's code. | ||
| What we're doing is passing an error back to the user but only *after* we have | ||
| allowed the rest of the user's code to execute. By using `process.nextTick()` | ||
| we guarantee that `apiCall()` always runs its callback *after* the rest of the | ||
| user's code and *before* the event loop is allowed to proceed. To acheive | ||
| this, the JS call stack is allowed to unwind then immediately execute the | ||
| provided callback which allows a person to make recursive calls to nextTick | ||
| without reaching a `RangeError: Maximum call stack size exceeded from v8`. | ||
|
|
||
| This philosophy can lead to some potentially problematic situations. Take | ||
| this snippet for example: | ||
|
|
@@ -169,15 +284,75 @@ names themselves won't change. | |
| to reason about (and it leads to code that's compatible with a wider | ||
| variety of environments, like browser JS.)* | ||
|
|
||
| ## Two reasons to use `process.nextTick()`: | ||
| ## Why use `process.nextTick()`? | ||
|
|
||
| There are two main reasons: | ||
|
|
||
| 1. Allow users to handle errors, cleanup any then unneeded resources, or | ||
| perhaps try the request again before the event loop continues. | ||
|
|
||
| 2. If you were to run a function constructor that was to, say, inherit from | ||
| `EventEmitter` and it wanted to call an event within the constructor. You | ||
| can't emit an event from the constructor immediately because the script | ||
| will not have processed to the point where the user assigns a callback to | ||
| that event. So, within the constructor itself, you can use | ||
| `process.nextTick()` to set a callback to emit the event after the | ||
| constructor has finished, which provides the expected results. | ||
| perhaps try the request again before the event loop continues. | ||
|
|
||
| 2. At times it's necessary to allow a callback to run after the call | ||
| stack has unwound but before the event loop continues. | ||
|
|
||
| One example is to match the user's expectations. Simple example: | ||
|
|
||
| ```js | ||
| var server = net.createServer(); | ||
| server.on('connection', function(conn) { }); | ||
|
|
||
| server.listen(8080); | ||
| server.on('listening', function() { }); | ||
| ``` | ||
|
|
||
| Say that listen() is run at the beginning of the event loop, but the | ||
| listening callback is placed in a setImmediate. Now, unless a hostname | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All instances of `setImmediate()` |
||
| is passed binding to the port will happen immediately. Now for the | ||
| event loop to proceed it must hit the `poll` phase, which means there | ||
| is a non-zero chance that a connection could have been received allowing | ||
| the connection event to be fired before the listening event. | ||
|
|
||
| Another example is running a function constructor that was to, say, | ||
| inherit from `EventEmitter` and it wanted to call an event within the | ||
| constructor: | ||
|
|
||
| ```js | ||
| const EventEmitter = require('events'); | ||
| const util = require('util'); | ||
|
|
||
| function MyEmitter() { | ||
| EventEmitter.call(this); | ||
| this.emit('event'); | ||
| } | ||
| util.inherits(MyEmitter, EventEmitter); | ||
|
|
||
| const myEmitter = new MyEmitter(); | ||
| myEmitter.on('event', function() { | ||
| console.log('an event occurred!'); | ||
| }); | ||
| ``` | ||
|
|
||
| You can't emit an event from the constructor immediately | ||
| because the script will not have processed to the point where the user | ||
| assigns a callback to that event. So, within the constructor itself, | ||
| you can use `process.nextTick()` to set a callback to emit the event | ||
| after the constructor has finished, which provides the expected results: | ||
|
|
||
| ```js | ||
| const EventEmitter = require('events'); | ||
| const util = require('util'); | ||
|
|
||
| function MyEmitter() { | ||
| EventEmitter.call(this); | ||
|
|
||
| // use nextTick to emit the event once a handler is assigned | ||
| process.nextTick(() => { | ||
| this.emit('event'); | ||
| }.bind(this)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @evanlucas good catch. Arrow functions can't have their
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @evanlucas ! replaced the arrow function with a plain-old anonymous function. |
||
| } | ||
| util.inherits(MyEmitter, EventEmitter); | ||
|
|
||
| const myEmitter = new MyEmitter(); | ||
| myEmitter.on('event', function() { | ||
| console.log('an event occurred!'); | ||
| }); | ||
| ``` | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lowercase
following