Lightweight Threads in JavaScript

© 2012 Martin Rinehart

Device requirements: Enough width to see both ends of at least one "pool".

Prerequisites: Good understanding JavaScript objects.

The Olympics are on! I'm all red, white and blue, patriotically cheering my countrymen and women. Across the pond the Brits are reminding me that they've been red, white and blue since before we were colonies. (Did I hear a Frenchman say "tricolor"? OK, that's Lochte in white, Agnel in blue.)

So start the races! Start the top set from the top down. That race is rigged. Each swimmer goes the same distance each time its thread runs, but red's thread runs every 50 milliseconds. White's thread runs every 48 millis. Blue, 46 millis, will always win, if you have patience.

The group in the pool on the bottom gets a random shot of extra juice when they hit the right end of the pool.


Each "swimmer" in the pool is controlled by a program running on a separate thread. But that's impossible! Better take a look behind your monitor for wires. Or are those invisible threads?

JavaScript does not have threads. Brendan Eich, JavaScript's inventor and now chairman of TC 39, the commmittee responsible for the international JavaScript standard, says we'll get threads "over my dead body."

It's certainly possible for a beginning programmer to make a mess using threads. Having one thread waiting on a second while the second thread waits on the first will bring a program to a halt. Having a program produce results that depend, in part, on which of two threads happens to run first (called a "race condition") is seldom what you want.

That said, Eich's attitude is arrogant—condescending in the extreme. "I'm smart enough to program with threads but you aren't." JavaScript programmers are at least as smart as Java programmers, (or Ruby, Python, C++, ...) who have all enjoyed the power of threads for years.

JavaScript actually has "lightweight" threads and every JavaScripter who has used 'setTimeout()' calls to move something around the screen has been using these threads. This article presents a small package that provides a thread-style API, built on JavaScript's threads.

Lightweight Threads

In languages which support both, we speak of "lightweight" and "heavyweight" threads. The former are built within the language and run within a single operating system thread. The latter are external, controlled by the operating system. Heavyweight threads have a major advantage: they can support multiple CPUs.

With a multicore CPU, your computer can actually be doing two or more things at once. You can be writing a poem or a program with one CPU, while another calculates the effect of global warming on the Himalayan glaciers.

Heavyweight threads typically take a lot of resources to create and can be a bit cumbersome when it comes to starting and stopping. Spreading computation-intensive programs among multiple CPU cores is an ideal application for heavyweight threads.

With a single core CPU, or with a language which only supports lightweight threads, your writing will share a CPU with the program that works on global warming. Let's do a quick calculation to see how this might work out.

Assume that you can type six characters in a second. Assume that it takes 100,000 instructions to take a character from the keyboard, put it into a data structure and then display the results on your monitor. How many 'clocks' are available for other uses if your CPU processes two billion instructions per second? Writing that poem is taking 100,000 of each 333 million clocks. There are 333+ million clocks left over (your poem is roundoff error) for other uses.

JavaScript Has Lightweight Threads

I was working with a newbie, trying to get him pointed in the right direction with some code he had found somewhere on the web. It had this line:

  setTimeout( func_name, 0 );

That stopped me. Either the person who wrote it had no clue, or the author was an ace from whom I should be learning. That line says, "execute the 'func_name' function after zero milliseconds delay." This is subtly different from:

  func_name();

The latter begins executing func_name immediately. It will run to completion before the next line (the one after the call to func_name) is executed. You can think of it as a call to a modal function: alert( "Important fact, here!" );. Contrast this to writing Important fact, here! into some span that is on your screen. In the latter case, your program does not wait for you to click "OK". It continues. With an alert() your program comes to a dead halt while it waits for you to click "OK".

Setting a setTimeout() for zero seconds from now says to JavaScript, "Execute this function as soon as you have a moment." In fact, you can think of it as launching a function on a new, concurrent thread.

Today's JavaScript threads are really pseudo-concurrent. Each will run to completion without giving the others a chance. If you launch three functions on zero-milli setTimeout()s, chances are pretty good that the launching code will run to completion, then the launched code will run, in the order they were launched. There is, however, no law preventing some implementor from making an improvement in this situation.

The "Thread" API

This little package steals a really excellent idea from Java: the Runnable interface. In Java, a class implements Runnable if it has a public method, run(). (That's right, the interface has just one method.) After that, the rest of our API could hardly be simpler. These are the basics:

API Basics

You construct a thread by calling Thread with any Runnable object. An object is Runnable if it has a run() method. Unlike Java, we don't worry about method signatures, so your run() may have zero or more parameters. Zero (the only choice in Java) is often best.

You start a Thread by calling its start() method with a numeric argument for millis. That will call the Runnable's run() once every millis milliseconds. (More precisely, it will call run() and then, after run() has finished, it will setTimeout() the next run() after a millis delay.)

The stop() method does just what you think. If you like details, it sets a flag that will, when the next run() wakes up, cancel the operation and stop the "set another run" process.

API Advanced Features

These are hardly "advanced" features, but you can safely ignore them when you are getting started.

Calling the start() method without an argument is a) useless, or b) invaluable. It's effect is to call Runnable.run(), which you should do directly, if that is all you want. On the other hand, if you are debugging new code, calling start() for a single run may be just what the code doctors ordered.

Adding a second argument to start() specifies the number of times the thread will call Runnable.run(). This is helpful if you don't want the thread to run forever but have no particular "stop now" condition in mind. We'll use this when the starter tells our swimmers to "Get set!".

The sleep() allows you to temporarily pause a thread without the inconvenience of stopping and starting it again.

API Non-Features

There is no priority system. These "threads" should not be used for any run() method that might make a pig of itself, taking all the available time. Keep the run() methods short.

There is no yield() method. In most thread systems this method allows other, possibly lower-priority threads to have a turn. After each run() completes its task, yield() is implied. Again, keep the run() methods short.

Thread Animation Basics

My first use for this thread API was in animation. I thought it might make animation simpler. It exceeded my expectations. Let's start with a definition: "animation" means incremental changes over time. You start here; you move or swell or recolor or whatever, just a wee bit. Then a wee bit more, repeating as needed to get to your end result. This is not rocket science. The typical JavaScript solution involves closures, which is a bit closer to rocket science than necessary.

Here are the Thread-based steps:

  1. Give your object a Thread property.
  2. Write a "small change" method.
  3. Ask the thread to run the "small change" method a few times.

Yes, it's that simple. Let's take a look at one example: having our second set of "swimmers" respond when the starter says, "Get set!".

Giving Swimmers Threads

We could share a Thread among our swimmers, but these are so lightweight that it's easier to give each swimmer its own thread. This is the code:

var swimmers = [];
for ( var i = 0; i < 3; i++ ) {
    /* assign colors, then
      assign start positions
    */
    swimmers[ i ].thread =
        new Thread( swimmers[i] );
}

If you're really looking critically, you noticed that we created the threads with references to the swimmers, but the swimmers don't implement Runnable. They don't have run() methods. This is a cheat. The threads will call their swimmers' run() methods when they start. We'll make them Runnable just in time.

Make Them Runnable

When you click the button that says "On Your Marks" the swimmers go to their places. The button swaps in "Get Set!" for it's new label. When you click "Get Set!" the start_click() function calls its get_set() inner function. This is our first bit of animation for these swimmers. They contract, preparing to spring forward. This is dead simple to code, as the only thing you need to change is their width. (Their feet, aka style.left, don't move.) Here's the shrink() method:

    function shrink() {
        var swimmer = this;
        swimmer.width -= 2.5;
        swimmer.div.style.width =
            swimmer.width + 'px';
    }

First, this function assigns a name, "swimmer," to the mysterious "this." JavaScript's this has many issues, so it's always a good idea to give it a name. Each swimmer has a width property. This routine simply decrements the width and then assigns it to the style.width property with the appropriate 'px' suffix. (Note: it is much simpler to have a numeric property, separate from style.width. If you depend on the style.width you have to parseInt() to get the numeric value before you can decrement it.)

Now, the only issue remaining is to call the shrink() routine several times in rapid succession and we will have an animated "Get Set!" for our swimmers. Begin by using the shrink() function as the run() method for the swimmers and then start() the thread:

    swimmer.run = shrink;
    swimmer.thread.start( 20, 16 );

That bit of code will shrink() a swimmer 16 times, separating the shrink()s by 20 milliseconds each. (That's about as fast as you can run JavaScript animation. See "Animation Speed" below.)

After the starter fires his pistol (you click "Go!") we will replace the shrink() routine with the race() routine and our tireless swimmers will begin doing laps.

Advanced Thread Animation

There really isn't much more to animation than you've just seen. If you think you can do it, you can!

Here are some pointers.

Animation Speed

Movies are commonly shot at 24 frames per second (42 millis per frame). This is more than enough for most animation.

Windows basic timer ticks every 16.7 millis, which is the same as a monitor's common 60 times per second refresh rate. Updating your screen faster than this will not improve your animation. (Battery manufacturers will love you. Your viewers may not.)

JavaScript setTimeout() calls may use a more accurate clock than its setInterval() calls. These threads use setTimeout(), though it may not help animations.

Programmers who are working on animations are far more sensitive to minor bits of flicker than the overwhelming majority of viewers. Get a second opinion.

Summary

If you use the pseudo-threads in this package for small tasks, like moving "swimmers" in a pool, you can forget that they are really just simplified versions of true lightweight threads. They work. If you want something more sophisticated, like threads that can communicate with each other, I recommend that you start by sending Eich a letter. Then get coding because standards committees are slower than Himalayan glaciers.


The Code

The Thread class has been extracted from my personal library and included on this page. It's free for adding threads to your sites and apps. Before you use it you will want to find/replace all "MRlib323" with your own namespace object name.

Feedback: MartinRinehart at gmail dot com.

# # #