You really need to stop thinking that everything is about threads. Almost all asynchronous operations in javascript is executed on the main thread.
Note: There will be people who tell you this is wrong, that javascript uses multiple threads and point to node.js documentation about V8. I am telling you that they are wrong. Node.js do run additional threads but they are only for disk I/O (all disk I/O are executed on one disk I/O thread, not one thread per file), DNS (because DNS APIs are blocking), crypto functions (because it uses CPU rather than I/O) and zip compression (same reason as crypto). Everything else including network I/O, mouse and keyboard handling, setTimeout etc. don't run on separate threads. You can read more about this on node's own documentation about the event loop: https://nodejs.org/en/docs/guides/dont-block-the-event-loop/
Asynchronous code in javascript mostly refer to what C programmers call non-blocking I/O. Blocking I/O stops your process until data is available:
// Blocking I/O pseudocode
data = read(file);
Non-blocking I/O returns immediately and does not return the data available. Instead it begins the process of fetching data:
// Non-blocking I/O (javascript's internal asynchronous) pseudocode
beginReading(file);
while (waiting) {
if (readyToRead(file)) {
data = read(file);
}
}
The advantage of non-blocking I/O compared to blocking I/O is that while the OS tells your device driver to fetch some bytes from the file and your device driver begins a PCI transaction and the PCI bus communicates with your disk controller and your disk controller begins addressing operation on the storage medium.. while all that is happening (which is a long time in CPU time).. you can execute code:
// Blocking I/O pseudocode
data = read(file); // several million instructions could have been executed
// but our process is blocked waiting for data
// Non-blocking I/O (javascript's internal asynchronous) pseudocode
beginReading(file);
while (waiting) {
if (readyToRead(file)) {
data = read(file);
}
else {
executeMoreCode(); // continue processing javascript while waiting
}
}
In C/C++ most people would hardcode (as in, actually write the executeMoreCode()
above) unless they are comfortable working with function pointers (the syntax is absolutely horrible). And even then, C/C++ does not offer an easy way to redefine that function after you've compiled your program (clever people can do wonders with interfaces - consider printer drivers which can be loaded after Windows have been compiled - but it's still complicated).
Javascript has first-class functions so most javascript API allow you to pass a callback as an argument to a function call that starts the non-blocking request.
Internally this is what javascript does:
// pseudocode:
do {
eventHandlers = executeJavascript();
// end of execution
events = waitForAllIO(); // this actually blocks but it is waiting
// for ALL I/O instead of just one
if (events.timeout) {
foreach (callback from eventHandlers) {
if (callback is TIMEOUT_HANDLER) {
callback(events.timeout);
}
}
}
else {
foreach (event from events) {
foreach (callback from eventHandlers) {
if (callback is for event) {
callback(event);
}
}
}
}
} while (eventHandlers.length > 0)
This loop goes by many names. Adobe Flash (which like node.js is an ECMAScript language that is just slightly different from browser javascript) calls it the "elastic racetrack". Most people just call it the event loop.
As you can see, nothing in the above logic requires additional threads. This architecture is so successful in handling concurrency that javascript implement threads (web workers & worker threads) as passing events back to the main thread thus even with threading javascript generally avoid locking (which is why neither web workers nor worker threads have locking mechanisms built in).
The thing to remember as a javascript programmer is:
Javascript is a strictly synchronous language. All lines of code are executed sequentially in order just like C or Java or Python.
Function definitions and reference are not called when defined or passed as argument to other functions just like C or Java or Python.
Asynchronous code does not execute code in parallel it only waits for events in parallel. 100% of the speedup of programs written in something like node.js is due to being able to make 1000 parallel I/O requests and wait for all of them at the same time. The I/O requests will be executed by hardware eg. hard disk, routers or external processes eg. SQL servers, Google, not javascript. Thus javascript does not need to execute any parallel code to get advantages of parallelism.
There is nothing magical about all this. You will face the same asynchronous behavior if you writ GUI code in C++ or Java using frameworks like GTK or WxWidgets or Swing.
I've written much more detailed explanations to specific questions on this subject. If you want to know more you may find my answers to other questions of interest to you:
Is there any other way to implement a "listening" function without an infinite while loop?
Does javascript process using an elastic racetrack algorithm
node js - what happens to incoming events during callback excution
I know that callback function runs asynchronously, but why?
Performance of NodeJS with large amount of callbacks
Is nodejs representing Reactor or Proactor design pattern?