ActionScript workers add threading to AS3 so that you can take advantage of today’s multi-core CPUs. I’ve written a couple of articles about them so far, but skipped over the basics of actually setting them up and using them. This is surprisingly tricky! Read on for a crash course on how to use workers to speed up your app.

An ActionScript worker is embodied by the Worker class. One worker represents one thread. A thread is like the code you’re used to writing, except that it runs at the same time on one of the CPU’s other cores. The code in your main SWF (e.g. the one you put in an HTML page) is the “main thread” and all the other threads are “worker threads”.

To create a Worker, you call the constructor and pass a SWF as a ByteArray. This is a really strange way to instantiate a class. You cannot simply pass a function to act as the entry point of your new thread. This leads to two distinct ways of organizing your code for workers.

The first way is what I call the “all-in-one” approach. In this approach the bytes of your normal, main thread SWF are used to create the Worker. Since the same code can be either the main thread or a worker thread, an if statement is used to detect which of these modes the code is running as. Thankfully, the worker API makes this easy. Here’s an example:

public class MainThread extends Sprite
{
	public function MainThread()
	{
		if (Worker.current.isPrimordial)
		{
			startMainThread();
		} 
		else
		{
			startWorkerThread();
		}
	}
 
	private function startMainThread(): void
	{
		// Use our own SWF bytes to create the worker thread
		var worker:Worker = WorkerDomain.current.createWorker(loaderInfo.bytes);
 
		// ... more main thread setup
	}
 
	private function startWorkerThread(): void
	{
		// ... worker thread setup
	}
}

This approach allows you to keep all of your code in one codebase, just like you’re used to with single-threaded code. Unfortunately, you’ve just mingled the main thread code with the worker thread code. So you’ll probably want to use classes to at least keep the code in different files.

The other approach is to have to main classes: one for the main thread and one for the worker thread. You compile the worker thread’s main class, compile the main thread’s main class with the worker thread embedded, then use those embedded bytes to create the worker thread. Here’s how that looks:

////////////////
// MainThread.as
////////////////
public class MainThread extends Sprite
{
	[Embed(source="WorkerThread.swf", mimeType="application/octet-stream")]
	private static var WORKER_SWF:Class;
 
	public function MainThread()
	{
		var workerBytes:ByteArray = new WORKER_SWF() as ByteArray;
		var worker:Worker = WorkerDomain.current.createWorker(workerBytes);
 
		// ... more main thread setup
	}
}
//////////////////
// WorkerThread.as
//////////////////
public class WorkerThread extends Sprite
{
	public function WorkerThread()
	{
		// ... worker thread setup
	}
}

Both approaches have upsides and downsides. You may choose your approach based on whichever organization structure makes the most sense for you and your project. The only real technical difference is that the “two SWF” method will have your other AS3 code compiled into both SWFs, thus increasing the total SWF size. However, that’s often not much of a concern these days as pure-code SWFs tend to be quite small.

The next step is to create MessageChannel objects so that your threads can communicate with each other. This isn’t strictly necessary, but almost all multi-threaded code will need some way for the threads to collaborate. The MessageChannel class represents an asynchronous communication between the threads, similar to socket communication. Most objects sent over the MessageChannel will be copied as the threads do not usually have direct access to each others’ objects.

MessageChannel on its own isn’t very useful. A MessageChannel object needs to be placed into a “shared property” that both threads can access. Think of it like a drop box: one thread places a named MessageChannel and the other retrieves it by that name. The two threads now use this MessageChannel to send and receives messages between them.

The main thread and the worker threads set up their MessageChannel objects in different ways. Here’s how it looks:

// Set up a MessageChannel to send messages from the main thread to a worker thread
// Since this is called from the main thread, Worker.current is the main thread
var mainToWorker:MessageChannel = Worker.current.createMessageChannel(worker);
worker.setSharedProperty("mainToWorker", mainToWorker);
 
// Set up a MessageChannel to receive messages from a worker thread into the main thread
var workerToMain:MessageChannel = worker.createMessageChannel(Worker.current);
workerToMain.addEventListener(Event.CHANNEL_MESSAGE, onWorkerToMain);
worker.setSharedProperty("workerToMain", workerToMain);
function onWorkerToMain(ev:Event): void
{
}

Because the threads are executing at the same time, you need to pay close attention to when the message channels are set as shared properties and when they are retrieved. You don’t want one thread to overwrite another thread’s MessageChannel and you don’t want to retrieve a MessageChannel that hasn’t been set.

To avoid this setup problem, I recommend a simple strategy: the main thread creates all of the MessageChannel objects and sets them as shared properties before the worker thread is started. Here’s how that process goes:

////////////////
// MainThread.as
////////////////
public class MainThread extends Sprite
{
	[Embed(source="WorkerThread.swf", mimeType="application/octet-stream")]
	private static var WORKER_SWF:Class;
 
	var mainToWorker:MessageChannel;
	var workerToMain:MessageChannel;
 
	public function MainThread()
	{
		var workerBytes:ByteArray = new WORKER_SWF() as ByteArray;
		var worker:Worker = WorkerDomain.current.createWorker(workerBytes);
 
		// Send to worker
		mainToWorker = Worker.current.createMessageChannel(worker);
		worker.setSharedProperty("mainToWorker", mainToWorker);
 
		// Receive from worker
		workerToMain = worker.createMessageChannel(Worker.current);
		workerToMain.addEventListener(Event.CHANNEL_MESSAGE, onWorkerToMain);
		worker.setSharedProperty("workerToMain", workerToMain);
	}
 
	private function onWorkerToMain(ev:Event): void
	{
	}
}
//////////////////
// WorkerThread.as
//////////////////
public class WorkerThread extends Sprite
{
	var mainToWorker:MessageChannel;
	var workerToMain:MessageChannel;
 
	public function WorkerThread()
	{
		// Receive from main
		// Since this is called from the worker thread, Worker.current is the worker thread
		mainToWorker = Worker.current.getSharedProperty("mainToWorker");
		mainToWorker.addEventListener(Event.CHANNEL_MESSAGE, onMainToWorker);
 
		// Send to main
		workerToMain = Worker.current.getSharedProperty("workerToMain");
	}
 
	private function onMainToWorker(event:Event): void
	{
	}
}

After the main thread has created and set its MessageChannel objects, it’s time to start the worker thread. That’s as easy as calling thread.start(), but how do you know when the worker thread has finished its work starting up? If the main thread immediately starts sending it messages the worker thread may not have even set up its event listeners on the MessageChannel objects. In this case the messages wouldn’t be received and potentially important tasks for the worker thread may be missed.

To avoid this situation I recommend a “startup” MessageChannel. This MessageChannel is, like all the other MessageChannel objects, created by the main thread for one purpose: to receive a message from the worker thread indicating that it’s finished starting up and is ready to be used. The following code shows how that works, including your first glimpse at actually sending and receiving messages using a MessageChannel:

////////////////
// MainThread.as
////////////////
public class MainThread extends Sprite
{
	[Embed(source="WorkerThread.swf", mimeType="application/octet-stream")]
	private static var WORKER_SWF:Class;
 
	var mainToWorker:MessageChannel;
	var workerToMain:MessageChannel;
	var workerToMainStartup:MessageChannel;
 
	public function MainThread()
	{
		var workerBytes:ByteArray = new WORKER_SWF() as ByteArray;
		var worker:Worker = WorkerDomain.current.createWorker(workerBytes);
 
		// Send to worker
		mainToWorker = Worker.current.createMessageChannel(worker);
		worker.setSharedProperty("mainToWorker", mainToWorker);
 
		// Receive from worker
		workerToMain = worker.createMessageChannel(Worker.current);
		workerToMain.addEventListener(Event.CHANNEL_MESSAGE, onWorkerToMain);
		worker.setSharedProperty("workerToMain", workerToMain);
 
		// Receive startup message from worker
		workerToMainStartup = worker.createMessageChannel(Worker.current);
		workerToMainStartup.addEventListener(Event.CHANNEL_MESSAGE, onWorkerToMainStartup);
		worker.setSharedProperty("workerToMainStartup", workerToMainStartup);
 
		worker.start();
	}
 
	private function onWorkerToMain(ev:Event): void
	{
	}
 
	private function onWorkerToMainStartup(ev:Event): void
	{
		var success:Boolean = workerToMainStartup.receive() as Boolean;
		if (!success)
		{
			// ... handle worker startup failure case
		}
	}
}
//////////////////
// WorkerThread.as
//////////////////
public class WorkerThread extends Sprite
{
	var mainToWorker:MessageChannel;
	var workerToMain:MessageChannel;
	var workerToMainStartup:MessageChannel;
 
	public function WorkerThread()
	{
		// Receive from main
		mainToWorker = Worker.current.getSharedProperty("mainToWorker");
		mainToWorker.addEventListener(Event.CHANNEL_MESSAGE, onMainToWorker);
 
		// Send to main
		workerToMain = Worker.current.getSharedProperty("workerToMain");
 
		// Send startup message to main
		workerToMainStartup = Worker.current.getSharedProperty("workerToMainStartup");
		workerToMainStartup.send(true);
	}
 
	private function onMainToWorker(event:Event): void
	{
	}
}

You may be tempted to use the built-in WorkerState events to synchronize your thread startup. It’s very appealing to avoid the extra steps you see above by simply listening for the WorkerState.RUNNING event like so:

// In the main thread...
worker = new Worker(workerBytes);
worker.addEventListener(Event.WORKER_STATE, onWorkerState);
function onWorkerState(ev:Event): void
{
	if (worker.state == WorkerState.RUNNING)
	{
		worker.start();
		// ... start using the worker
	}
}

Do not be fooled! The RUNNING state only means that the Worker has been created and begun executing its constructor. It may or may not have finished its constructor, so you will most likely get intermittent problems due to the worker thread not having finished setting itself up. A “startup” MessageChannel avoids this possibility and ensures a smooth thread startup.

Now that you’ve safely created your worker thread established communication via a MessageChannel, you’re ready to start actually using the thread. This part is totally up to your application, but in general you want to hand the worker thread big tasks via MessageChannel.send() and then receive the results via MessageChannel.receive(). The possibilities are endless here, so let your imagination run wild. Just remember, if you don’t use any worker threads you are potentially letting half (dual core), three-quarters (quad core), or even seven-eighths (octo-core) of the CPU sit idle.

Spot a bug? Have a question or suggestion? Post a comment!