52

I've been working on a multi-threaded JavaScript runtime implementation for the past week. I have a proof of concept made in C++ using JavaScriptCore and boost.

The architecture is simple: when the runtime finishes evaluating the main script it launches and joins a thread-pool, which begins picking tasks from a shared priority queue, if two tasks try to access a variable concurrently it gets marked atomic and they contend for access.

Working multithreaded node.js runtime

The problem is that when I show this design to a JavaScript programmer I get extremely negative feedback, and I have no idea why. Even in private, they all say that JavaScript is meant to be single threaded, that existing libraries would have to be rewritten, and that gremlins will spawn and eat every living being if I continue working on this.

I originally had a native coroutine implementation (using boost contexts) in place as well, but I had to ditch it (JavaScriptCore is pedantic about the stack), and I didn't want to risk their wrath so I decided against mentioning it.

What do you think? Is JavaScript meant to be single threaded, and should it be left alone? Why is everyone against the idea of a concurrent JavaScript runtime?

Edit: The project is now on GitHub, experiment with it yourself and let me know what you think.

The following is a picture of promises running on all CPU cores in parallel with no contention:

Running promises concurrently.

26
  • 7
    This seems like a highly opinionated question. Did you ask the people who apparently didn't like your idea why they think it will be troublesome?
    – 5gon12eder
    Commented Apr 12, 2016 at 4:50
  • 26
    Adding threads to something that isn't meant to be multithreaded is like converting a one lane road into an expressway without providing driver's ed. It'll work pretty well most of the time, until people start crashing randomly. With multithreading, you're either going to have subtle timing bugs that you can't reproduce or erratic behavior most of the time. You have to design with that in mind. You need thread synchronization. Just making variables atomic doesn't eliminate race conditions.
    – mgw854
    Commented Apr 12, 2016 at 4:51
  • 3
    How do you plan on handling multi-threaded access to shared state? "Marked as atomic and contend for access" does not explain how you think this would really work. I would guess that negative attitudes toward the idea are because people have no idea how you'd actually make this work. Or, if you're putting all the burden on the developer like in Java or C++ to use proper mutexes and such, then people are probably thinking why do they want that complication and programming risk in an environment that is free from it.
    – jfriend00
    Commented Apr 12, 2016 at 4:59
  • 17
    Because automatically coordinating random state between threads is considered a nearly impossible problem so you have no credibility that you can offer anything that does it automatically. And, if you're just going to put the burden back on the developer like Java or C++ do, then most node.js programmers don't want that burden - they like that node.js doesn't have to deal with that for the most part. If you want a more sympathetic ear, you will have to explain/show how and what you would offer in this regard and why it would be good and useful.
    – jfriend00
    Commented Apr 12, 2016 at 5:27
  • 3
    Please continue your work. I consider languages without multi-threading as toy languages. I think most JavaScript developers work with the browser, which has a single-thread model.
    – Chloe
    Commented Apr 12, 2016 at 6:28

8 Answers 8

71

1) Multithreading is extremely hard, and unfortunately the way you've presented this idea so far implies you're severely underestimating how hard it is.

At the moment, it sounds like you're simply "adding threads" to the language and worrying about how to make it correct and performant later. In particular:

if two tasks try to access a variable concurrently it gets marked atomic and they contend for access.
...
I agree that atomic variables won't solve everything, but working on a solution for the synchronisation problem is my next goal.

Adding threads to Javascript without a "solution for the synchronisation problem" would be like adding integers to Javascript without a "solution for the addition problem". It's so fundamental to the nature of the problem that there's basically no point even discussing whether multithreading is worth adding without a specific solution in mind, no matter how badly we might want it.

Plus, making all variables atomic is the sort of thing that's likely to make a multithreaded program perform worse than its singlethreaded counterpart, which makes it even more important to actually test performance on more realistic programs and see if you're gaining anything or not.

It's also not clear to me whether you're trying to keep threads hidden from the node.js programmer or if you plan on exposing them at some point, effectively making a new dialect of Javascript for multithreaded programming. Both options are potentially interesting, but it sounds like you haven't even decided which one you're aiming for yet.

So at the moment, you're asking programmers to consider switching from a singlethreaded environment to a brand new multithreaded environment that has no solution for the synchronisation problem and no evidence it improves real-world performance, and seemingly no plan for resolving those issues.

That's probably why people aren't taken you seriously.

2) The simplicity and robustness of the single event loop is a huge advantage.

Javascript programmers know that the Javascript language is "safe" from race conditions and other extremely insidious bugs that plague all genuinely multithreaded programming. The fact that they need strong arguments to convince them to give up that safety does not make them closed-minded, it makes them responsible.

Unless you can somehow retain that safety, anyone who might want to switch to a multithreaded node.js would probably be better off switching to a language like Go that's designed from the ground up for multithreaded applications.

3) Javascript already supports "background threads" (WebWorkers) and asynchronous programming without directly exposing thread management to the programmer.

Those features already solve a lot of the common use cases that affect Javascript programmers in the real world, without giving up the safety of the single event loop.

Do you have any specific use cases in mind that these features don't solve, and that Javascript programmers want a solution for? If so, it'd be a good idea to present your multithreaded node.js in the context of that specific use case.


P.S. What would convince me to try switching to a multithreaded node.js implementation?

Write a non-trivial program in Javascript/node.js that you think would benefit from genuine multithreading. Do performance tests on this sample program in normal node and your multithreaded node. Show me that your version improves runtime performance, responsiveness and usage of multiple cores to a significant degree, without introducing any bugs or instability.

Once you've done that, I think you'll see people much more interested in this idea.

10
  • 1
    1) Okay, I'll admit that I've been putting off the issue of synchronization for a while now. When I say that 'two tasks will contend', that's not my design, it's mainly an observation: dl.dropboxusercontent.com/u/27714141/… ­— I'm not sure what kind of witchcraft JavaScriptCore is performing here, but shouldn't this string get corrupted if it weren't inherently atomic? 2) I strongly disagree. This is the reason JS is looked upon as a toy language. 3) ES6 promises would be much more performant if implemented using a thread pool scheduler. Commented Apr 12, 2016 at 18:26
  • 13
    @voodooattack "This is the reason JS is looked upon as a toy language." No, this is the reason you consider it a toy language. Millions of people use JS every day and are perfectly happy with it, shortcomings and all. Make sure you're solving a problem that enough other people actually have, that isn't better solved by just changing languages. Commented Apr 12, 2016 at 20:51
  • @ChrisHayes The question is, why put up with its shortcomings when I can fix them? Would concurrency as a feature improve JavaScript? Commented Apr 12, 2016 at 23:14
  • 1
    @voodooattack That's the question. Would concurrency as a feature improve Javascript? If you can get a community answer to that that isn't "no", then maybe you're on to something. However, it seems as though the event loop and Node's native delegation to worker threads for blocking events suffice for most of what people need to do. I think that if people really need threaded Javascript, they will use Javascript workers. If you can find a way to make workers work with first-class functions instead of JS files, however....then you might really be onto something.
    – user26452
    Commented Apr 13, 2016 at 2:29
  • 7
    @voodooattack People who call JavaScript a toy language don't know what they're talking about. Is it the go-to language for everything? Of course not, but dismissing it like that is certainly a mistake. If you want to dispel the notion that JS is a toy language, then make a non-trivial, production application in it, or point to an existing one. Just adding concurrency won't change those people's minds, anyway.
    – jpmc26
    Commented Apr 13, 2016 at 2:43
16

A decade or so ago Brendan Eich (the inventor of JavaScript) wrote an essay called Threads Suck, which is definitely one of the few canonical documents of JavaScript's design mythology.

Whether it is correct is another question, but I think it had a big influence on how the JavaScript community thinks about concurrency.

13
  • 31
    The last person I would take advice from is Brendan Eich. His whole career is based on creating JavaScript, which is so bad we have a heap of tools created to try and get around its innate crapness. Think of how many developer hours have been wasted in the world because of him. Commented Apr 12, 2016 at 6:42
  • 12
    While I understand why you would discount his opinion in general, and even your disdain for JavaScript, I don't understand how your perspective would allow for disregarding his expertise in the domain. Commented Apr 12, 2016 at 7:10
  • 4
    @BillyCravens haha, even the canonical book on Javascript : "Javascript the good parts" by Crockford gives the game away in the title. Treat Eich's attitudes the same way, just stick to the stuff he says that is good :-)
    – gbjbaanb
    Commented Apr 12, 2016 at 7:29
  • 13
    @PhilWright: His first implementation of Javascript was a Lisp variant. Which to me earns him huge respect. His boss's decision to force him to replace Lisp syntax with C-like syntax is not his fault. Javascript at its core is still a Lisp runtime.
    – slebetman
    Commented Apr 12, 2016 at 9:59
  • 7
    @PhilWright: You should not blame Eich for the crappyness of the language. It's bugs in early implementations, and the need of backwards-compatibility for a web language that prevented JS from maturing. The core concepts still form a wonderful language.
    – Bergi
    Commented Apr 12, 2016 at 14:03
16

Just guessing here to demonstrate a problem in your approach. I can't test it against the real implementation as there is no link anywhere...

I'd say it is because invariants are not always expressed by the value of one variable, and 'one variable' is not sufficient to be the scope of a lock in the general case. For example, imagine we have an invariant that a+b = 0 (a bank's balance with two accounts). The two functions below ensure that the invariant is always held at the end of each function (the unit of execution in single-threaded JS).

function withdraw(v) {
  a -= v;
  b += v;
}
function deposit(v) {
  b -= v;
  a += v;
}

Now, in your multithreaded world, what happens when two threads execute withdraw and deposit at the same time? Thanks, Murphy...

(You might have code that treats += and -= specially, but that is no help. At some point, you will have local state in a function, and with no way to 'lock' two variables at the same time your invariant will be violated.)


EDIT: If your code is semantically equivalent to the Go code at https://gist.github.com/thriqon/f94c10a45b7e0bf656781b0f4a07292a, my comment is accurate ;-)

10
  • This comment is irrelevant, but the philosophers are capable of locking multiple objects, but they starve because they lock them in an inconsistent order. Commented Apr 12, 2016 at 12:35
  • 3
    @voodooattack "I just tested..." Have you never encountered inconsistent execution order with thread scheduling? It varies from run to run, from machine to machine. Sitting down and running a single test (or even a hundred tests!) without identifying the mechanism for how operations are scheduled is useless.
    – jpmc26
    Commented Apr 13, 2016 at 2:41
  • 5
    @voodooattack The problem is that simply testing concurrency isn't useful. You need to be able to prove that the invariant will hold, or the mechanism can never be trusted in production.
    – sapi
    Commented Apr 13, 2016 at 2:53
  • 1
    But you haven't given the user any tools to "use the system responsibly" because there's absolutely no locking mechanism. Making everything atomic gives an illusion of thread-safety (and you take the performance hit of everything being atomic, even when you don't need atomic access), but it doesn't actually solve most concurrency problems like the one thriqon gives here. For another example, try iterating over an array on one thread while another thread adds or removes elements from the array. For that matter, what makes you think the engine's implementation of Array is even thread safe? Commented Apr 13, 2016 at 4:39
  • 2
    @voodooattack So if users can only use your threads for functions with no shared data or side effects (and they'd better not be using them for anything else, because as we've seen here, there's no way to ensure thread safety), then what value are you providing over Web Workers (or one of the many libraries that provide a more usable API around Web Workers)? And Web Workers are much safer, because they make it impossible for the user to use the system "irresponsibly." Commented Apr 13, 2016 at 4:48
8

Atomic access does not translate into thread-safe behavior.

One example is when a global data structure needs to be invalid during an update like rehashing a hashmap (when adding a property to an object for example) or sorting a global array. During that time you cannot allow any other thread to access the variable. This basically means that you need to detect the entire read-update-write cycles and lock over that. If the update is non trivial that will end up into halting problem territory.

Javascript has been single threaded and sandboxed from the beginning and all code is written with these assumptions in mind.

This has a great advantage with regards to isolated contexts and letting 2 separate contexts run in different threads. I also means that people writing javascript don't need to know how to deal with race conditions and various other multi-thread pitfals.

1
  • This is why I think the scheduler API should be more on the advanced side, and internally used for promises and functions that have no side effects. Commented Apr 12, 2016 at 23:18
6

Is your approach going to significantly improve performance?

Doubtful. You really need to prove this.

Is your approach going to make it easier/faster to write code?

Definitely not, multithreaded code is many times harder to get right than single threaded code.

Is your approach going to be more robust?

Deadlocks, race conditions etc. are a nightmare to fix.

4
  • 2
    Try building a raytracer using node.js, now try it again with threads. Multi-process is not everything. Commented Apr 12, 2016 at 6:21
  • 8
    @voodooattack, no one in their right mind would write a ray-tracer using javascript, with or without threading, because it is a relatively simple, but computation-intensive algorithm that is better written in a fully compiled language, preferably one with SIMD support. For the kind of problems that javascript is used for, multi-process is more than enough.
    – Jan Hudec
    Commented Apr 12, 2016 at 11:59
  • @JanHudec: Heh, JS is going to get SIMD support as well :-) hacks.mozilla.org/2014/10/introducing-simd-js
    – Bergi
    Commented Apr 12, 2016 at 14:05
  • 2
    @voodooattack If you're not aware of them, take a look at SharedArrayBuffers. JavaScript is getting more concurrency constructs, but they're being added extreme cautiously, addressing specific pain points, trying to minimize making bad design decisions that we'll have to live with for years.
    – Jeremy
    Commented Apr 13, 2016 at 2:20
2

Your implementation is not just about introducing concurrency, rather its about introducing a specific way to implement concurrency i.e concurrency by shared mutable state. Over the course of history people have used this type of concurrency and this has lead to many kinds of problems. Ofcourse you can create simple programs that works perfectly with using shared mutable state concurrency but the real test of any mechanism is not what it can do but can it scale as the program get complex and more and more features are added to the program. Remember software is not a static thing that you build once and are done with it, rather it keeps evolving over time and if any mechanism or concept can't cope with evolution of the software then it will only lead to new problems and this is exactly what history has taught us about shared mutable memory concurrency.

You can look at other models of concurrency (ex: message passing) to help you figure out what sort of benefits those models provide.

8
  • 1
    I think ES6 promises would benefit from my implementation's model, since they don't contend for access. Commented Apr 12, 2016 at 6:10
  • Have a look at WebWorkers specification. There are npm packages that provide their implementation but you can implement them as the core of your engine rather as a package
    – Ankur
    Commented Apr 12, 2016 at 6:26
  • JavaScriptCore (the webkit implementation of JS I'm using) already implements them, it's just a compilation flag. Commented Apr 12, 2016 at 6:28
  • Ok. WebWorkers are concurrency with message passing. You can try your raytracer example with them and compare it with the mutable state approach.
    – Ankur
    Commented Apr 12, 2016 at 6:31
  • 4
    Message passing is always implemented on top of mutable state, which means that it would be slower. I fail to see your point here. :-/ Commented Apr 12, 2016 at 6:34
1

This is needed. The lack of a low level concurrency mechanism in node js limits it's applications in fields such as math and bioinformatics, etc... Besides, concurrency with threads doesn't necessarily conflict with the default concurency model used in node. There are well known semantics for threading in an environment with a main event loop, such as ui frameworks (and nodejs), and tho they are definitely overly complex for most situations they still have valid uses.

Sure, your average web app will not require threads, but try doing anything a little less conventional, and the lack of a sound low level concurrency primitive will quickly stir you away to something else that does offer it.

6
  • 4
    But "math and bioinformatics" would be better written in C#, JAVA, or C++
    – Ian
    Commented Apr 12, 2016 at 11:35
  • Actually Python and Perl are the dominant languages in those areas.
    – dryajov
    Commented Apr 12, 2016 at 15:03
  • 1
    Actually, the main reason I'm implementing this is because of machine learning applications. So you do have a point. Commented Apr 12, 2016 at 18:39
  • @dryajov Be glad that you don't know how big FORTRAN is in some of the computational science areas... Commented Apr 12, 2016 at 20:18
  • @dryajov Because they're more accessible to Humans Who Maybe Aren't Programmers Full Time, not because they're inherently better at genome sequencing -- we have purpose-built languages like R and compiled scientific langs like Fortran for that.
    – cat
    Commented Apr 12, 2016 at 20:32
0

I really believe it's because it's a different and powerful idea. You are going against belief systems. Stuff becomes accepted or popular through network affects not on the basis of merit. Also no one wants to adapt to a new stack. People automatically reject things that are too different.

If you can come up with a way to make it into a regular npm module which sounds unlikely, then you may get some people using it.

3
  • Do you mean that JS programmers are vendor-locked to npm? Commented Apr 12, 2016 at 6:11
  • 3
    The question is about a new runtime system and not some npm module and also mutable shared state concurrency is really an old idea.
    – Ankur
    Commented Apr 12, 2016 at 6:55
  • 1
    @voodooattack I mean they are brainlocked to the approach that is already popular and it's going to be next to impossible to overcome the status quo bias. Commented Apr 12, 2016 at 8:01

Not the answer you're looking for? Browse other questions tagged or ask your own question.