I was SO excited when I first read this article, as I was at the time implementing shared-global multithreading in a fork of JS Interpreter [1][2] and it was thrilling to think that we might one day have true parallelism in a real JS engine, not just simulated concurrency in our educational toy.
Since then I've often wondered if anyone at Apple was still working on this, or if it was just one of those things (like proper tail call support in V8) that was destined never to see the light of day.
A year or so ago I tried tracking it down again (apparently I'd not bookmarked it at the time) but alas several search engines responded only with a sea of articles about web workers.
Finally, last week I put Gemini on the case and, despite it claiming that it didn't exist and that I must be conflating memories of some other related articles it did correctly identify you as the author, after which it was easy to find the link to the original article on your blog.
Since re-reading it I've been wondering if it might be possible to implement it with help from AI (not having written any C++ since before the turn of the century I don't think I'd be too successful doing it unassisted!), or whether JSC's internals might have drifted too far in the intervening years.
It's delightful that someone else has take a stab at it, and I look forward to seeing where this leads.
Thanks for all the work you did laying the groundwork that made it feasible to even contemplate, then contemplating all tricky details and writing the answers down in the form of such an inspiring article.
Yes, you did. And it's a good design. You even did the GC question justice.
My concern is more in the spirit of "Your scientists were so preoccupied with whether or not they could, they didn't stop to think if they should.". Of course JS being single threaded wasn't a hard constraint. Lift it, and people like you can use the parallelism to do great things.
The problem is that most developers are not you. Shared memory concurrency is foot-artillery (especially if truly parallel). Adding threads to the JS ecosystem is selling W48 nuclear artillery shells at the toy store.
JS's ostensible limitation to a single thread forced users to do what they should have been doing anyway: message-passing, thread-per-core architecture, and actor-ish stuff. People who don't know better reach for shared memory concurrency because it seems like a good way to solve problems, but it's actually a dangerous attractor in idea space. JS engine limitations were accidentally keeping people away from it. Now that they can hear the siren's song of a mutex, they'll run around on the hard problems of parallel programming.
Now, that's not a reason to avoid shipping such a system. It's just not something I would have chosen to implement for the masses.
I don’t think it is extreme. Imagine this is added to WebKit. Now I have a new question to answer - can I use library X across multiple threads? How do I know it does not have a little cache inside which breaks if I call it from multiple threads?
Another issue is lack of memory model (sorry if ai missed it) which means memory updates will be published to threads differently on different architectures.
And then an obvious problem of mixing async with locks - never ends good.
I think with ES6 and newer things really cleaned up and now we’re left with avoidable ugly parts, of which every language has.
Before when you didn’t even have strict equality checking, for example, you were forced to know about implicit type casting.
Getting on the same page with modules also helped a lot. Typescript directly in Node is great. Look mom, no build system!! I’m just hoping one day browsers will accept TS the same way.
There’s a mode to pretend those features don’t exist and not allow them. Meaning it gets far simpler to just type elide rather than any actual compilation effort. I think this idea is getting more popular and it would be kinda nice if TS committed to not adding any more features like that.
TS has committed to not adding any more features like that. Features only get added when they reach a certain threshold on the TC39 standardisation track.
TS is JS just with stuff on top so it can’t really ever kill JS. The way Node does it is to just ignore the type notations in a TS file, making it valid JS. Does mean you can’t use things like enums but worth the price.
It's successful because it's been kept away from the kind of programmers who think the time spent to endlessly specify everything four times is nothing compared to the sadness of losing a byte or a cycle. These are the descendants of people who hundreds of years ago would have insisted that real work is in Latin. C++26 is available for them, or Node/React with hundreds of dependencies if they want JavaScript, or they can even compile and run whole operating systems into WASM now, or anything else. Just let JavaScript be the domain of people who do other things for fun.
Worth noting that javascript has had workers, shared memory and atomics for years and that you can use them today. Look at this guy writing a lockless allocator: https://greenvitriol.com/posts/lockless-allocator
The only difference in this PR is that it makes threads light (workers are fat because they carry a whole v8 instance with them) and it makes shared memory default with light threads (now you need to pass a shared array buffer first).
Javascript is probably not your first language, I get it, but it has had "the siren song of a mutex" for years now. What really surprises me and I can't explain is why you went and took time to express such strong opinions on something that you obviously don't even know or use that well.
js does not and has never had shared native objects between workers. there is a vast gulf between "here is a shared array buffer, feel free to interpret these bytes on another thread" and "your existing { ... obj } code just works but now is threaded".
shared array buffer is a decent primitive but nothing in the language uses it. if you want to make existing code that uses JS objects multi-threaded on top of shared array buffer, you might as well port it to Go -- it would be less work than rewriting it to use raw byte arrays.
There's a difference between 1) having a shared-everything heap and 2) having a separate, obscure facility (which practically nobody uses) for building a special data-only portal to shared memory. #1 normalizes the mutex. #2 doesn't.
I have strong opinions on the superiority of #2 to #1 because I've dealt with endless bugs caused by people who think they can handle #1 and can't. Reasoning about complex memory order rules and thread interleaving is extremely difficult for both humans and AIs. That's why we abstract over raw threads with actors, STM, fork/join facilities, and (my favorite) structured cooperative concurrency. It's not a knock against anyone's skill to point out that EVERYONE gets concurrency wrong and we need guardrails on top.
That said, let's be honest: the JS ecosystem has a culture that'll make #1 worse than it usually is. There's a certain combination of insularity and lack of restraint I've observed in the JavaScript world that prompts its members to re-learn the hard way all the painful lessons in software history.
#2 is very much used in high performance web apps, whenever you can't afford the overhead of message passing. I agree it's a specialized tool and normally you don't need it, but it's a misrepresentation to say "it's an obscure feature that almost nobody uses". If you do webGL, for example, you almost invariably end up using shared array buffers.
Years ago I did "multithreaded Javascript" by calling into Rhino (Javascript engine) from multiple threads. Granted, I converted Rhino from JVM to CLR, so it wasn't exactly a stable environment, but it did "work".
It’s certainly possible, but I worry that weird things can happen when doing something as “simple” as defining a property if another thread is messing with the prototype chain. Even thread safe property maps can’t entirely save you because operations that need to go up the prototype chain are not and cannot be atomic.
Okay. I gave it another read and I think we agree in general, but maybe disagree on how much memory model strangeness is acceptable, and how wide the gap between, “The behaviour can be understood by a developer,” and, “it should be possible to write a sequential JS program that creates an indistinguishable heap,” is likely to be.
The lower level bits round the object model etc. all look very solid.
Although structs may not be necessary to make JS concurrent their limitations might help in reducing where memory model strangeness could creep in.
I was SO excited when I first read this article, as I was at the time implementing shared-global multithreading in a fork of JS Interpreter [1][2] and it was thrilling to think that we might one day have true parallelism in a real JS engine, not just simulated concurrency in our educational toy.
Since then I've often wondered if anyone at Apple was still working on this, or if it was just one of those things (like proper tail call support in V8) that was destined never to see the light of day.
A year or so ago I tried tracking it down again (apparently I'd not bookmarked it at the time) but alas several search engines responded only with a sea of articles about web workers.
Finally, last week I put Gemini on the case and, despite it claiming that it didn't exist and that I must be conflating memories of some other related articles it did correctly identify you as the author, after which it was easy to find the link to the original article on your blog.
Since re-reading it I've been wondering if it might be possible to implement it with help from AI (not having written any C++ since before the turn of the century I don't think I'd be too successful doing it unassisted!), or whether JSC's internals might have drifted too far in the intervening years.
It's delightful that someone else has take a stab at it, and I look forward to seeing where this leads.
Thanks for all the work you did laying the groundwork that made it feasible to even contemplate, then contemplating all tricky details and writing the answers down in the form of such an inspiring article.
[1] https://github.com/NeilFraser/JS-Interpreter [2] https://github.com/google/CodeCity/blob/fa1bd2734b806559ffaf...
In case anyone missed it, this PR is based on that:
> This is an implementation of the design Filip Pizlo published in 2017: "Concurrent JavaScript: It Can Work!".
Yes, you did. And it's a good design. You even did the GC question justice.
My concern is more in the spirit of "Your scientists were so preoccupied with whether or not they could, they didn't stop to think if they should.". Of course JS being single threaded wasn't a hard constraint. Lift it, and people like you can use the parallelism to do great things.
The problem is that most developers are not you. Shared memory concurrency is foot-artillery (especially if truly parallel). Adding threads to the JS ecosystem is selling W48 nuclear artillery shells at the toy store.
JS's ostensible limitation to a single thread forced users to do what they should have been doing anyway: message-passing, thread-per-core architecture, and actor-ish stuff. People who don't know better reach for shared memory concurrency because it seems like a good way to solve problems, but it's actually a dangerous attractor in idea space. JS engine limitations were accidentally keeping people away from it. Now that they can hear the siren's song of a mutex, they'll run around on the hard problems of parallel programming.
Now, that's not a reason to avoid shipping such a system. It's just not something I would have chosen to implement for the masses.
I don’t understand the thread phobia
Comparing it to nukes is a bit extreme, don’t you think?
I don’t think it is extreme. Imagine this is added to WebKit. Now I have a new question to answer - can I use library X across multiple threads? How do I know it does not have a little cache inside which breaks if I call it from multiple threads?
Another issue is lack of memory model (sorry if ai missed it) which means memory updates will be published to threads differently on different architectures.
And then an obvious problem of mixing async with locks - never ends good.
> Comparing it to nukes is a bit extreme, don’t you think?
Does Herb Sutter strike you as extreme?
https://herbsutter.com/2013/02/11/atomic-weapons-the-c-memor...
This is consistent with the endless contempt people have had for JavaScript and those that use it.
Yeah I don’t get that either
It’s a super successful language
I think with ES6 and newer things really cleaned up and now we’re left with avoidable ugly parts, of which every language has.
Before when you didn’t even have strict equality checking, for example, you were forced to know about implicit type casting.
Getting on the same page with modules also helped a lot. Typescript directly in Node is great. Look mom, no build system!! I’m just hoping one day browsers will accept TS the same way.
You still need a compiler for TSX, though. There's also a tiny bit of non-erasable Typescript (enums).
There’s a mode to pretend those features don’t exist and not allow them. Meaning it gets far simpler to just type elide rather than any actual compilation effort. I think this idea is getting more popular and it would be kinda nice if TS committed to not adding any more features like that.
TS has committed to not adding any more features like that. Features only get added when they reach a certain threshold on the TC39 standardisation track.
> I’m just hoping one day browsers will accept TS the same way.
Wouldn't that be a direct kill of JS?
TS is JS just with stuff on top so it can’t really ever kill JS. The way Node does it is to just ignore the type notations in a TS file, making it valid JS. Does mean you can’t use things like enums but worth the price.
Did C++ kill C?
Valid point, though not a good comparison: You can learn C++ and have a productive career without ever learning or writing a single line of C.
When did JS not have strict equality?
1995-1999. Strict equality was introduced in ES3 which was first released in December 1999.
https://www-archive.mozilla.org/js/language/e262-3.pdf
The parent said 'before ES6', I suspect they were not thinking of 1999.
It's successful because it's been kept away from the kind of programmers who think the time spent to endlessly specify everything four times is nothing compared to the sadness of losing a byte or a cycle. These are the descendants of people who hundreds of years ago would have insisted that real work is in Latin. C++26 is available for them, or Node/React with hundreds of dependencies if they want JavaScript, or they can even compile and run whole operating systems into WASM now, or anything else. Just let JavaScript be the domain of people who do other things for fun.
Worth noting that javascript has had workers, shared memory and atomics for years and that you can use them today. Look at this guy writing a lockless allocator: https://greenvitriol.com/posts/lockless-allocator
The only difference in this PR is that it makes threads light (workers are fat because they carry a whole v8 instance with them) and it makes shared memory default with light threads (now you need to pass a shared array buffer first).
Javascript is probably not your first language, I get it, but it has had "the siren song of a mutex" for years now. What really surprises me and I can't explain is why you went and took time to express such strong opinions on something that you obviously don't even know or use that well.
js does not and has never had shared native objects between workers. there is a vast gulf between "here is a shared array buffer, feel free to interpret these bytes on another thread" and "your existing { ... obj } code just works but now is threaded".
shared array buffer is a decent primitive but nothing in the language uses it. if you want to make existing code that uses JS objects multi-threaded on top of shared array buffer, you might as well port it to Go -- it would be less work than rewriting it to use raw byte arrays.
There's a difference between 1) having a shared-everything heap and 2) having a separate, obscure facility (which practically nobody uses) for building a special data-only portal to shared memory. #1 normalizes the mutex. #2 doesn't.
I have strong opinions on the superiority of #2 to #1 because I've dealt with endless bugs caused by people who think they can handle #1 and can't. Reasoning about complex memory order rules and thread interleaving is extremely difficult for both humans and AIs. That's why we abstract over raw threads with actors, STM, fork/join facilities, and (my favorite) structured cooperative concurrency. It's not a knock against anyone's skill to point out that EVERYONE gets concurrency wrong and we need guardrails on top.
That said, let's be honest: the JS ecosystem has a culture that'll make #1 worse than it usually is. There's a certain combination of insularity and lack of restraint I've observed in the JavaScript world that prompts its members to re-learn the hard way all the painful lessons in software history.
#2 is very much used in high performance web apps, whenever you can't afford the overhead of message passing. I agree it's a specialized tool and normally you don't need it, but it's a misrepresentation to say "it's an obscure feature that almost nobody uses". If you do webGL, for example, you almost invariably end up using shared array buffers.
Years ago I did "multithreaded Javascript" by calling into Rhino (Javascript engine) from multiple threads. Granted, I converted Rhino from JVM to CLR, so it wasn't exactly a stable environment, but it did "work".
That's excellent work and a great read, Filip!
It’s certainly possible, but I worry that weird things can happen when doing something as “simple” as defining a property if another thread is messing with the prototype chain. Even thread safe property maps can’t entirely save you because operations that need to go up the prototype chain are not and cannot be atomic.
My blog post explains how to make prototype chain operations work in the presence of threads
Okay. I gave it another read and I think we agree in general, but maybe disagree on how much memory model strangeness is acceptable, and how wide the gap between, “The behaviour can be understood by a developer,” and, “it should be possible to write a sequential JS program that creates an indistinguishable heap,” is likely to be.
The lower level bits round the object model etc. all look very solid.
Although structs may not be necessary to make JS concurrent their limitations might help in reducing where memory model strangeness could creep in.
This won’t work well without a few other things, like structs
https://tc39.es/proposal-structs/
Structs aren’t necessary for my proposal to work well
I remember reading that article some years ago.
Boy, wasn't I surprised when I ran into this PR. I'm excited.