
JavaScript in Parallel: Web Workers and SharedArrayBuffer - avgp
http://50linesofco.de/post/2017-02-06-javascript-in-parallel-web-workers-transferables-and-sharedarraybuffer
======
i_s
It is really how disappointing the options to share data with workers are in
the browser.

Combine the limited options options to transfer data quickly, and the limited
API available in the worker itself (no version of a DOM, even a gutted one for
doing measurements), it is no surprise how few opportunities to use them
effectively there are.

When it comes to transferring objects, it is so bad people are resorting to
JSON.stringifying messages. [0]

Seems like it would be easy to just add an Immutable Array and Map to the
standard library, and let people use that on workers without these silly
limitations. What am I missing?

[0] [https://nolanlawson.com/2016/02/29/high-performance-web-
work...](https://nolanlawson.com/2016/02/29/high-performance-web-worker-
messages/)

~~~
rl3
I used web workers extensively in a JS project a little over a year ago, and
it was a nightmare. Granted what I was attempting was crazy, but it shouldn't
have been as painful as it was.

Basically the "gotchas" are in the browser implementations. Each browser's web
worker implementation is a little different, and those little differences
(bugs) have huge implications in terms of what you can and can't do. For
example: one browser may relay direct worker-to-worker messages via the main
thread, so if your main thread is blocked your workers can't talk to each
other, and that largely defeats the entire purpose of direct worker-to-worker
communication.

Fortunately some of these issues have been fixed since (including that one, if
memory serves), but it's slow going. I believe what happened was workers were
introduced 5+ years ago, met with little interest initially, and the APIs have
rotted for years until recently when interest picked up again, especially
since SharedArrayBuffer.

My qualm with SharedArrayBuffer is that it kind of sucks to use in pure JS
projects, because you have to serialize/deserialize everything to and from the
buffer. With the Emscripten toolchain's pthreads support, as far as I'm aware
you just compile your code and the heap lives inside SharedArrayBuffer. You
don't have to write boilerplate serialization code, so compared to plain
JavaScript it's seamless experience in that regard.

My advice to anyone using web workers in a pure JavaScript project is to use
them as their name implies: offloading long-running calculations. If you try
to treat them as true threads, you're going to have a bad time. Especially if
your inter-worker messaging volume is high and you have frequent
interdependent (blocking) calculations.

That said, the work I did was prior to SharedArrayBuffer. For very high-
performance projects, it will likely be prudent to use SharedArrayBuffer
itself as a messaging medium between workers.

~~~
nojvek
Ios has a ui thread and a separate thread for application logic. React native
makes good use of this to make applications silky smooth.

In Web land, the overhead of communication with workers is costly that it
makes sense to do things in main thread. Preact had an worker diffing
implementation but that was slower.

We once had to unzip on worker and pass to main thread, it was faster just to
do it on UI thread.

------
albertTJames
If multithread is a possibility for the future of javascript why not make
promises and async multithread and keep the same syntax we are using now...
What is there to gain with workers ? It seems to me like an unnecessary
addition ... but I am interested in the point of view of specialists on the
matter. I may be wrong, but promises and async are for me a great formalism
upon which one could build a future version of js that is multithreaded.

~~~
sebringj
Ah so you mean call other js processes through async/yield type stuff. I think
that is cognitively easier than introducing new concepts like this has with a
familiar way to do things. I agree with you. There would have to be some other
syntax like "thread" to note it is not in the main thread but acted just like
async or something.

var somevalue = async doSomething();

var someExpensiveVAlue = thread doExpensiveThing();

var lotsOfExpensiveThings = Thread.all(threads);

~~~
voxic11
This is how it works in dotnet.

var val = await Task.Run(() => doSomeThreadedWork(param));

Indicates to the runtime that the specified delegate may be run on another
thread. This doesn't explicitly start a new thread but rather just allows the
delegate to execute on one of the thread pool threads that is managed by the
runtime. It uses heuristics to decide how many threadpool threads to maintain
and whether to actually use one of them to execute your delegate. A similar
model would be very useful to have in JavaScript.

~~~
tracker1
The issue is that in .Net there are locking primatives to access shared
values... where as in Node/JS you would need something that only allowed
passing of strings, other primatives, and SharedArrayBuffer or similar objects
that don't change underneath unexpectedly.

------
ic4l
I kept finding myself needing to toss stuff in a background thread, and ended
up making this:
[https://github.com/icodeforlove/task.js](https://github.com/icodeforlove/task.js)

It makes wrangling multiple workers much easier, and doesn't require you to
have external JS files. (also works in node.js)

demo:
[http://s.codepen.io/icodeforlove/debug/ZOjBBB/NjrYzwzWdzLA](http://s.codepen.io/icodeforlove/debug/ZOjBBB/NjrYzwzWdzLA)

~~~
ralusek
Looks impressive, well thought out interface. That's also an impressive grid
of browser compatibility.

------
impostervt
What do people actually use Web Workers for? The examples I've seen, including
this one, seem contrived. I keep hoping they'll change it to allow background
image manipulation, but I haven't seen much real progress in that front.

~~~
Klathmon
I've used it for background image manipulation before.

Paint the image to a canvas, then grab the imagedata off of it, split it into
as many parts as you have threads, then use the "transferrableObjects"
property of postMessage to zero-copy transfer the data to each worker to be
processed, transferred back, and re-stitched together.

It's pretty powerful and suprisingly easy to work with once you understand it.

[0] is a snippet from the code, but be gentile... It was a personal project
where I was trying out polymer 0.5 and made a lot of questionable design
choices...

Also, I've heard of the idea of using webworkers as a "first class" platform.
That is do all of the core parts of your application in them and only use the
"main" thread as a "ui" thread. I haven't gotten a chance to try it out, but
it seems like a great idea that could really work well in some SPAs.

[0]
[https://github.com/Klathmon/stitchpics/blob/master/app/eleme...](https://github.com/Klathmon/stitchpics/blob/master/app/elements/cross-
stitch/element.js#L95-L118)

~~~
bartread
That's interesting but, AFAIK (and, believe me, I'd be happy to be corrected),
what you can't do is create a canvas element (even one not attached to the
DOM), and paint directly to it using the standard 2D context and drawing
primitives.

Like I say, more than happy to be corrected, because that sort of thing would
be incredibly helpful. Really, anything that lets you mess with a disconnected
DOM in the background and then attach it in the foreground could be useful but
(and, again, I'm very happy to be corrected), I don't think you can do this.

~~~
vanderZwan
You're talking about OffscreenCanvas. It's available in Firefox and nowhere
else:

[https://developer.mozilla.org/en-
US/docs/Web/API/OffscreenCa...](https://developer.mozilla.org/en-
US/docs/Web/API/OffscreenCanvas)

Having said that, if you want fast image manipulation, you're probably better
off doing direct manipulation of Uint8Clamped arrays anyway, since that can be
much faster.

~~~
Klathmon
And if you can get away with it and don't mind a lot of bit shifting, it's
even better working with the Uint32array which packs all 3 colors and alpha
into one element which reduces your loops by 4X

~~~
vanderZwan
Because matching the native 32 word size is better for the prefetcher, right?

Wouldn't most CPU's these days be smart enough to detect advancing the index
by 4, and then using offsets?

So:

    
    
        for(let i = 0; i < someUint8Array.length; i += 4){
          let R = someUint8Array[i], G = someUint8Array[i+1],
            B = someUint8Array[i+2], A = someUint8Array[i+3];
          // ... manipulations here
        }

~~~
Klathmon
I'm honestly not sure why it is, but across all browsers on both ARM and
x86_64 arches it was almost 3x faster than doing what you wrote.

I have a feeling it's more of a JS JIT thing than a CPU prefetcher thing, but
honestly I'm not really sure.

In my program I linked above, it was actually faster to use Uint32array
everywhere and then use functions to pull the 4 color values from it and
another function to push the 4 values back to a uint32.

Granted, it's been over a year since I last benchmarked that code, but I did
reuse some of the image code recently and found iterating over a Uint32array
to be significantly faster. (And funnily enough, manually unrolling the loop
of Uint32array to something similar to what you wrote gave an additional small
performance boost, but it was small enough to be not worth the extra weirdness
in the code to me)

~~~
vanderZwan
Thanks for the info, that might save me some benchmarking time myself in the
near future ;)

~~~
Klathmon
If it helps, I took the class I made that converts between 4 Uint8Clamped
values to a Uint32 value and vice versa into an NPM package at [0].

At the very least it can show you some of the gotchas with bit shifting in JS
(like how values often look negative until they are placed into a Uint32array
then they become positive integers, and how you need to check for endianness)

[0]
[https://github.com/Klathmon/BitPacker.js](https://github.com/Klathmon/BitPacker.js)

~~~
vanderZwan
Would not have thought of checking for endianness, thanks!

------
franciscop
If you don't like to have to load an external file (+ an extra request) you
can use my library uwork:
[https://github.com/franciscop/uwork](https://github.com/franciscop/uwork)

It has some limitations, but for long lived process intensive and async
functions is perfect. It has a really clean and easy syntax where you don't
need to learn everything about Web Workers and their APIs to be able to use
it.

If you are already using promises you might not even need anything besides
wrapping the function in a callback.

------
z3t4
This is a very nice article! There's however a bug in the code, where it will
"finish" when the last worker is done:

    
    
      if(msg.data.offset + msg.data.length === buffer.byteLength)
    

While you most likely want to wait for _all_ workers to be done before showing
the results. In this code however, the last worker will always finish last
because it has more work to do (higher numbers).

It's often cheaper to scale horizontally, by spreading the work between
physical machines, then to add more cores to a shared memory. So it's not such
a big deal to have a single threaded program, and single threaded code is
easier to reason about. SharedArrayBuffer will however be nice in JavaScript
because it allows optimization is games and such, allowing you to have
parallel for loops.

------
buttershakes
Workers are really bad. Its anecdotal but I've written several projects and
more often then not there is a weird browser quirk, a memory leak, or some
other nastiness hidden in the worker implementation. I think my next project
I'll go with Emscripten pthreads implementation and see if it's better.

------
euroclydon
I'd just like some high level guidance on how my browser makes use of threads
internally. I feel like I could have further optimized a couple web apps with
that knowledge.

~~~
Waterluvian
Excluding web workers, just one thread per tab/window. So there's really no
consideration for optimization via. parallelism unless you use web workers.

And if I'm wrong, this is the quickest way to get the right info. :)

~~~
baddox
I'm pretty sure you're wrong. At the bare minimum, setTimeout must use a clock
in another thread, since it doesn't block your primary thread code. I'm fairly
sure that most of all of the JavaScript APIs that use callbacks are using
additional threads, including the ubiquitous XMLHttpRequest.

~~~
Waterluvian
I'm not sure how the inner plumbing works. But since the queue just pops onto
the stack, there's no true parallelism and therefore no optimization
opportunity.

That being said, I'm not sure you're correct. There is no concern of blocking
the main thread, since functions queued up via. `setTimeout()` are not tracked
by some non-blocking timer. The queue is only inspected if the stack is empty.
So if anything is happening, we just ignore the queue until nothing is
happening. `setTimeout()` only guarantees a message will be processed after x
milliseconds, not on-time.

[https://developer.mozilla.org/en/docs/Web/JavaScript/EventLo...](https://developer.mozilla.org/en/docs/Web/JavaScript/EventLoop)

~~~
baddox
You can definitely block the main thread. XHR requests can be run
synchronously by calling .open() with false as the third argument. And when
you do asynchronous XHR requests, the browser can and will run multiple
requests in parallel. Of course, the callbacks just get added to the event
queue and run one at a time to completion on the main thread. And granted,
it's hard to call this an "optimization opportunity," since you should almost
never make synchronous XHR requests anyway.

~~~
Waterluvian
True. But I'd call synchronous XHR a legacy item that you should probably
never use.

------
SureshG
A dart example -
[https://github.com/filiph/prime_finder](https://github.com/filiph/prime_finder)

~~~
markdog12
I don't think your example uses SharedArrayBuffer? Nice to have Dart example
though.

------
quadyeast
<= instead of < would make the first few numbers come out correctly:

for(var n=2; n <= Math.floor(Math.sqrt(candidate)); n++)

------
tjfontaine
I actually misread the title as "JavaScript in Peril", which depending on you
feel about the features ...

------
ww520
What are the statuses of Web Worker support in the mobile browsers? Android?
iOS?

~~~
nreece
All browsers (except Opera Mini) support it:
[http://caniuse.com/#feat=webworkers](http://caniuse.com/#feat=webworkers)

------
jpalomaki
+1 for providing good summary in the beginning. I wish more articles had
these.

