"Single threaded" in this case means that it runs YOUR Javascript in a single thread. That means that it runs a piece of your Javascript until it returns control back to the system and only then can it run the code attached to some other event. No two pieces of your Javascript are ever actually executing at the same time.
FYI, we're ignoring WorkerThreads here for the purposes of this discussion which are a purposeful way to run multiple threads of Javascript, but are not involved in the general asynchronous architecture.
Asynchronous operations are ALL implemented in native code (usually C/C++ code) by nodejs. They are things such as timers, networking operations, disk operations, etc... That native code (mostly in a cross platform library called libuv
) has interfaces in Javascript that allows you to call them such as http.request()
or fs.read()
, but the underlying core implementation of those functions is in native code. The native code for those operations may or may not actually use OS threads in its implementation. But, those threads are entirely hidden from the Javascript code.
For example, networking operations in nodejs do not use threads. They use natively asynchronous APIs in the operating system that allow them to be notified when a network operation has completed without blocking or spinning and waiting for it. Other asynchronous operations such as file operations do actually use a pool of native OS threads to "simulate" an asynchronous architecture.
So, while your Javascript is run as single threaded, multiple asynchronous operations can be in process at once. If you look at this code:
const fsp = require('fs').promises;
Promise.all([fsp.readFile("filea.txt"), fsp.readFile("fileb.txt")]).then(results => {
console.log(results[0]);
console.log(results[1]);
}).catch(err => {
console.log(err);
});
This will read two files in parallel and tell you when the data from both files is available. While your Javascript still runs as single threaded, the underlying file operations are using OS-level threads and the disk operations are running in separate threads. So, it's only your actual Javascript that is single threaded, not necessarily the underlying asynchronous operations.
Fortunately, you can generally avoid entirely the concurrency headaches of multi-threaded programming because two pieces of your own Javascript are never running at the same moment in time. Thus, you don't need mutexes or other concurrency devices just to write to variables safely. The details of thread use are abstracted behind the asynchronous interfaces. And, the event driven nature of how nodejs handles the completion of these asynchronous interfaces dictates that one piece of Javascript will run to complete before the next completion event can be processed that has the next asynchronous result in it.