ActionScript Workers Tutorial
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!
#1 by henke37 on October 14th, 2013 ·
The thing is, it is more like an entirely separate process than a thread. Especially with the extremely limited data passing.
Also, you typoed a “two” into a “to”.
#2 by Clark on October 14th, 2013 ·
Thanks Jackson. I have been seeing your other entries for the last few weeks but it was beyond me. I will take a read of this tonight. Great Introduction to this topic!
#3 by Frank on October 14th, 2013 ·
Excellent article! Thank you very much Jackson. Now I just hope that Adobe will finish mobile workers in the near future.
#4 by benjamin guihaire on October 14th, 2013 ·
In the first way, the “all-in-one†approach, all the static in classes are going to get initialized twice, right ? (once for each thread)
class A
{
static protected var b:Boolean = initializeB();
protected function initializeB():Boolean
{
trace(“initializeB called”);
return true;
}
}
If true, on large projects, it looks like the all-in-one approach should really be avoided.
#5 by jackson on October 15th, 2013 ·
Yes. It will be like your main SWF is running twice but just happens to have executed some code differently based on whether it’s the main thread. That means you’ll get duplicates of all the classes (i.e. one in each
ApplicationDomain
) and all the statics will be initialized separately. Nothing is shared between the two.The all-in-one is probably better suited to simple tests than large projects because it’s an easier project to set up given that it only involves one compile step to one SWF. It’s probably not so bad if you can stand the overhead mentioned above which, in fairness, is probably less than a few milliseconds of initialization time and a megabyte of memory. On a modern desktop that’s definitely acceptable on a “loading” screen. This makes it a tempting tradeoff in favor of project simplicity that I’m sure many will be willing to accept.
#6 by Dennis on October 22nd, 2013 ·
But aren’t Workers “user mode” threads? What still means that the OS can’t seperate them in to different processes running on different kernels, because the thread table is only known in the flash runtime and not by the OS?
If yes you should have different threads running in the same process, or not?
#7 by jackson on October 22nd, 2013 ·
No, they’re real kernel/OS-level threads.
#8 by Peter Strømberg on October 23rd, 2013 ·
Your explanation is brilliant. I might even dare to try it out now :O)
It does however expose the half-ass way this has been implemented in AS3. It’s like starting 2 flash players and have them communicate via Local Connection.
Strange you need your worker class to be a swf rather than a class, and makes this a relatively clumsy way of working. Surely this could have been handled by the compiler rather than the coder?
Still, I guess we got what we asked for, multi-threading.
#9 by arash on November 4th, 2013 ·
Thank u for your articles about workers .I am working on an air project and I have to use worker but when I use your sample in air project it doesn’t work . is there any extra setting when I use this in air project I using air SDK 3.8
#10 by jackson on November 4th, 2013 ·
According to the Worker documentation you can use them in Flash Player, AIR for desktops, and AIR for Android (as of AIR 3.9), but not iOS:
#11 by Otacon on December 5th, 2013 ·
Thanks, very good tutorial. I have a question though: in which environment did you get it working? Because I’m trying to use Workers in Flash Pro CC, but I have the following problems:
1) in debug mode Worker.isSupported returns true, but the background worker isn’t actually working (it doesn’t trace anything in the output)
(see for example http://stackoverflow.com/questions/18029142/worker-getting-null-when-retrieving-shared-property
-“I’ve concluded that this only happens when testing it in debug mode”)
2) in release version simply Worker.isSupported return false. (see http://forums.adobe.com/thread/1239692)
I’m neither of the two posters above, but I’m encountering both problems at the same time…!
Thanks in advance.
#12 by jackson on December 5th, 2013 ·
I probably tested in Google Chrome, but it may have been Firefox.
#13 by zag on December 11th, 2013 ·
lazy passing parameters, trough comfortable way :)
#14 by Alama on December 13th, 2013 ·
Adobe nous a fait là un truc bien lourd !! pourquoi faire simple quand on peut faire compliqué .. :( Une simple classe Thread aurait bien suffisant avec peut être une classe ThreadManager ..
#15 by jackson on December 13th, 2013 ·
I don’t speak French so I’m going off of Google Translate, but I think Adobe decided to put workers in an entirely different
ApplicationDomain
to reinforce their “shared nothing” model. This is totally different from the normal Java-styleThread
class approach where everything (i.e. all memory) is shared. In some senses it makes things more complicated since you need to pass messages back and forth between workers rather than directly changing a shared pool of memory. In other senses it makes things simpler since you don’t run into the classic threading errors where two threads are operating on the same memory and get out of sync with “race conditions”, “deadlocks”, and so forth. It’s a controversial decision in my opinion, so your complaints are totally valid.#16 by Ryan Elliott on March 13th, 2014 ·
Not sure if this is a typo but:
“workerToMainStartup.addEventListener(Event.CHANNEL_MESSAGE, onWorkerToMain);”
is setup to talk to onWorkerToMain and not onWorkerToMainSetup.
#17 by jackson on March 13th, 2014 ·
Thanks for catching this! I’ve corrected it in the article.
#18 by jp on October 9th, 2014 ·
The sample works fine but When I this class to my flex Project I always get Verify Error: MessageChannel could not be found.
I did add the swc file to my new project too. Is Actionscript worker compatible with only AIR applications?
Thanks for the tutorial!
#19 by jackson on October 9th, 2014 ·
One thing to do is to make sure your project is set up to use these features. They definitely work on web browsers, so it sounds like either a project configuration issue or a plugin version issue.
#20 by Jens Eckervogt on November 15th, 2015 ·
Hello dear i am trying to access to external swf to air with Filesystem example to write or read?
Possible with worker is more than LocalConnection or getDefinitionByName?
You know File class with getDefinitionByName works fine 100 %
But FileStream Class can not write, read and more with getDefinitionByName How do i fix?
I try to find good solution like LocalConnection, ExternalInterface, Worker or other ways if you know that.
If MainApp is air app and MyLoadedApp.swf is external swf without air libraries.
For example with worker:
var _w:Worker = WorkerDomain.current.createWorker(swfLoader.loaderInfo.bytes);
_w.start();
..
public function writeStringToFile(file:File, str:String):void
{
var stream:FileStream = new FileStream();
stream.open(file, FileMode.WRITE);
stream.writeUTFBytes(str);
stream.close();
}
And external swf:
var _workerToMain:MessageChannel;
…
protected function myClickHandler(evt:MouseEvent):void
{
_worker = WorkerDomain.current.createWorker(this.loaderInfo.bytes);
var _fileClass:Class = getDefinitionByName(_fileClassRef) as Class;
var _defaultClass:Class = getDefinitionByName(getQualifiedClassName(“TestAir”)) as Class;
var _textFile:* = _fileClass.userDirectory;
_textFile = _textFile.resolvePath(“writetext.txt”);
if(_textFile.exists)
{
_workerToMain = Worker.current.getSharedProperty(“writeStringToFile”);
_workerToMain.send(_textFile, “Test String was wrktten from external swf.”);
}
}
How do i fix if i have error because type is not defined….. Please explain me correct how do i know if i use worker and filestream access.
Thanks for answer
#21 by MyFlashLab on January 31st, 2016 ·
We would appreciate if you check this wrapper class library which hides all the complicated codes related to AS Workers and let Flash devs easily use threads. we have open sourced this library with MIT license. cheers http://www.myflashlabs.com/developer-friendly-as-worker-api/
#22 by jackson on January 31st, 2016 ·
Workers are the lowest-level concurrency API in Flash, so it’s nice to have libraries built on top of them to make them easier to use. There aren’t many that I know of, so it’s good to see that you’ve provided one and even better to see that it’s open-sourced under the MIT license.
#23 by Jay on May 4th, 2016 ·
Hi, quick question, I’m trying to pass a Flash MovieClip to a worker, however when I try to read what I got in the receiving end I get an “undefined” object. Strings/ints/primitives work though.
I tried using registerClassAlias, or writing the MC to a ByteArray, but neither worked.
If you happen to know if I can do that, would be great.
#24 by jackson on May 4th, 2016 ·
It can be helpful to think about passing objects between workers like passing objects over a network or to a file. First you have to convert the object into a byte array, transmit the bytes, then convert the byte array to an object. How would you do that with a
MovieClip
? It’s technically possible, but involves a huge amount of complexity so it’s not implemented by Adobe. Primitives, on the other hand, are trivial and are therefore supported.