1

It says in a lot of website that JavaScript is single-threaded. When they say this, do they mean the JavaScript runtime?

I might be misunderstanding something but isn't JavaScript just a programming language and the programs you create with it should be the ones labeled as single-threaded? But maybe I'm not understanding something so can someone please explain what I am don't getting?

g_b
  • 11,728
  • 9
  • 43
  • 80

2 Answers2

8

TL;DR

  1. JavaScript is single-threaded within a realm (realm ~= the global environment, variables, etc.).
  2. JavaScript uses a job queue to run code in a realm, and each job must run to completion before any other job in the realm can run.
  3. JavaScript programs can have multiple threads working in parallel, by having multiple realms ("workers").
  4. Realms (and therefore threads) can communicate, transfer objects to each other, and even share memory.
  5. Usually, the single-threaded rule applies only within a single realm, but sometimes realms are grouped together and only one of them can be serviced by a thread at any given time.

The above was only specified at the language level after this question was asked. It was introduced in the ES2017 spec because it was necessary in order to fully describe the semantics and memory model for shared memory. Prior to that, the word "thread" doesn't appear in any JavaScript specification (for instance, not in ES2016).

Details

When you asked your question in 2016, JavaScript the language was silent on the topic of threading and your interpretation that it was really host environments (browsers, Node.js, etc.) that defined whether the code was run single- or multi-threaded was correct. However, even then, all major JavaScript implementations were single-threaded within a realm.¹

But things have moved on. ES2017 defined the language as allowing a single active thread within a realm at any given time, in order to define the semantics and memory model for shared memory.

Let's look at those five bullet points above in more detail, starting with the first two:

  1. JavaScript is single-threaded within a realm (realm ~= the global environment, variables, etc.).

  2. JavaScript uses a job queue to run code in a realm, and each job must run to completion before any other job in the realm can run.

(1) means that when a thread is running code in a realm, no other thread can be running other code in that realm.

(2) means that once a "job" is started (for instance, the initial job running the script, or a job for running an event handler), that job must be run to completion before any other job can be started. (Without this rule, it would be possible for a single thread to start a job, suspend it in the middle, and run another job before finishing the first, all without violating the single-active-thread rule.)

These two things work together to make reasoning about JavaScript code fairly straightforward compared to shared-memory multi-threaded environments. Let's look at an example.

Suppose we have this code as part of a larger JavaScript program:

// Assume nothing assigns to `counter` except `increment` below
let counter = 0;

function increment() {
    if (counter < 10) {
        ++counter;
    }
}

// ...various places `increment` is called...

In a spec-compliant JavaScript environment, only one thread can be running code that can access counter at any given time (that is, only a single thread can be running code in the realm counter belongs to). And once the job that called increment starts, it has to run to completion before any other job can be started in the realm. Those two things together mean we know that counter will never reach 11. It will reach 10 if increment is called ten times, but it will never reach 11, no matter how many times increment is called.

That wouldn't be reliably true in a multi-threaded shared-memory environment (like the Java JVM, or multi-threaded C/C++ programs), for several reasons. One is that it would have a race condition between the counter < 10 check and the ++counter operation. Two different threads could each get past the counter < 10 check when counter was 9, then both execute the ++counter operation.² That isn't possible in JavaScript, though, because of its single-active-thread-per-realm specification and run-to-completion semantics. No other code can be running in the realm between the counter < 10 check and the ++counter operation. This makes reasoning about code in JavaScript much simpler than it is in shared-memory multi-threaded environments.

("But what about if counter is in shared memory?" I hear you ask. Good question! Indeed, then you have to worry about race conditions and all kinds of other dragons. This is one reason we have Atomics.compareExchange. See Chapter 16 of my book JavaScript: The New Toys for details.)

  1. JavaScript programs can have multiple threads working in parallel, by having multiple realms ("workers").

The browser environment, Node.js, Deno, etc. all have the concept of workers (web, Node.js, Deno). The main realm can create a worker, and since the main realm and worker realms are distinct, code in those realms can be running in parallel on different threads. That's fine, because there's still only a single thread active within a realm at any given time.

  1. Realms (and therefore threads) can communicate, transfer objects to each other, and even share memory.

Realms and threads can communicate via postMessage and similar, can transfer some kinds of objects to each other, and can use shared memory so that they literally share a block of memory. Note that shared memory comes with all kinds of dragons! Race conditions, out-of-order writes, all sorts of things. (Again, more details in that chapter on shared memory I mentioned earlier.)

  1. Usually, the single-threaded rule applies only within a single realm, but sometimes realms are grouped together and only one of them can be serviced by a thread at any given time.

One example where realms are grouped together in this way is when, in a browser, a window opens another same-origin child window:

const child = window.open("something.html");

Now, the parent and child windows have direct access to each other's realms via child (in the parent) and opener or parent (in the child). They can even call each other's global functions.

If different threads could be running the code in each of those realms in parallel, it would potentially violate the single-active-thread-per-realm rule and/or the run-to-completion rule if (say) the child called a method in the parent. To prevent that, the realms are grouped so that only one thread can be running code in any of them at any given time. This means only one realm can make forward progress at any given moment.


¹ Aside from some experimental projects, the only exception to that I'm aware of is Java's support for running "scripting" code in JavaScript using Rhino. The resulting code, running on the JVM, could be running multi-threaded.

² In practice, that specific example would be unlikely even in a shared-memory multi-threaded environment, but it's a reasonable example of the kind of thing that happens, without being too complicated.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
2

While TJC's answer is of course correct, I don't think it addresses the issue of what people actually mean when they say that "JavaScript is single-threaded". What they're actually summarising (inaccurately) is that the run-time must behave as if has a single thread of execution, which cannot be pre-empted, and which must run to completion. The actual runtime can do anything it likes as long as the end result behaves in this way.

This means that while a JavaScript program may appear to be massively parallel with lot of threads interacting with each other, it's actually nothing of the sort. A kernel controls everything using the queue, event loop, and run-to-completion semantics (briefly) described here.

This is exactly the same problem that Hardware Description Languages (VHDL, Verilog, SystemC (though not actually a language), and so on) face. They give the illusion of massive parallelism by having a runtime kernel cycle between 'processes' which are not pre-emptible, and which must run until defined suspend points. The point of this is to ensure that models are executed in a determinate, repeatable fashion.

The difference between the HDLs and JS is that this is very well defined and fundamental for HDLs, while it's glossed over for JS. This is an extract from the SystemC LRM, which covers it briefly - it's much better defined in the VHDL LRM, for example.

Since process instances execute without interruption, only a single process instance can be running at any one time, and no other process instance can execute until the currently executing process instance has yielded control to the kernel. A process shall not pre-empt or interrupt the execution of another process. This is known as co-routine semantics or co-operative multitasking.

AndresM
  • 1,293
  • 10
  • 19
EML
  • 9,619
  • 6
  • 46
  • 78
  • I wouldn't call it "glossed over" for JS at all, not [as of the latest specification](http://www.ecma-international.org/ecma-262/6.0/index.html#sec-jobs-and-job-queues). 'twas a bit vague (but well-established in some contexts) beforehand. – T.J. Crowder Jan 18 '16 at 11:44
  • Thanks, hadn't seen that. – EML Jan 18 '16 at 12:29