Simple Pseudo-threading in ActionScript
This post could be considered an addendum to our moving to flash series. I decided to post it because of the lack of good examples out there. I hope it helps out.
In moving from a multi-threaded environment to a single-threaded one you will commonly have to address how to handle long-running tasks in Flash. Since ActionScript is single-threaded, this means that we can’t just put the task on a background thread and handle it when it’s complete. If I had a feature request for the ActionScript language, it would be to make some of this easier (although I do understand why it isn’t there now, and I’d rather have real Abstract classes…).
If you happen to be looking for a way to do this, you might have run across a few other posts. All of them will tell you that you have to figure out how to break up your task into smaller bits that can be handled one frame at a time. They’ll probably also tell you that doing this for hierarchical tasks is really hard. I couldn’t find anyone that actually shows you how to do it though, so here goes:
The Hierarchical Operation
We do a lot of operations on XML hierarchies. Some of those hierarchies are fairly deep as well, so we need to be able to break up the task into multiple, smaller tasks. Let’s say that you have a function that looks like this:
public function runActionOnHierarchy(tRoot:IXmlNode, tAction:IXmlHierarchyAction) {
try {
tAction.nodeEncountered(tRoot);
}
catch (tEx:Error) {
// handle error
}
var tChildren:IObjectArray= tRoot.getChildren();
if(tChildren != null) {
var iSize:int= tChildren.size();
var tChild:IXmlNode;
for (var i:int=iSize-1; i>=0; --i) {
tChild = IXmlNode(tChildren.get(i));
runActionOnHierarchy(tChild, tAction);
}
}
}
This code uses some of our own interfaces but you should get the idea. This method just takes an action and applies it to every node in the hierarchy. Ideally, that action doesn’t take longer than a frame, or about 1/30th of a second (depending on your desired framerate, you can change this). If it does, you’ll have to break up the action itself but we’ll address that a little later.
The Pseudo-thread Task
Since we aren’t dealing with threads, we will refer to the operations we want to perform as tasks. Think of them as the separable operations that would have been part of a larger thread. Here is an interface for a task that will be run as part of a Pseudo-thread:
public interface IPseudoThreadTask {
/**
* Run the task.
* @return whether or not the task is finished.
*/
function run(tManager:PseudoThreadManager):Boolean;
}
Obviously, a pretty simple interface. The only part that might get confusing is that the run() method takes the PseudoThreadManager. This is actually pretty important. I don’t like that we have to inject this dependency into our core tasks but if this task is going to spawn other tasks, as will be the case in a hierarchical one, then you’ll need this so that you can add the new tasks to it (unless it’s a singleton and globally accessible, but some people don’t like using those).
The Pseudo-thread Manager
The pseudo-thread manager is pretty simple as well. We’ll use a stack to manage the tasks that need to be executed. As a particular task is running, it may spawn other tasks that will be added to the manager, but the original task itself may not have completed, so we’ll always use the top element of the stack when determining what to execute. A small note however: if the task does complete, you need to be using a data structure that is capable of removing an object from the middle of the structure and compressing the other elements. We do this through our own IMutableObjectArray interface. A linked list is another, potentially better, option.
public class PseudoThreadManager {
private static const THREAD_ALLOTMENT:Number = 1000 / 30; // 1/30th of a second.
/**
* The array of pseudo-thread tasks. This will be used as a stack.
*/
private var mtTasks:IMutableObjectArray = new MutableObjectArray();
/**
* Constructor.
*/
public function PseudoThreadManager() { }
/**
* Add a task to be managed by the task manager.
*/
public function addTask(tTask:IPseudoThreadTask):void {
mtTasks.add(tTask);
}
/**
* Run the Pseudo-thread task. This will execute tasks for an allotted amount of time.
*/
public function run():void {
var iTasks:int = mtTasks.size();
if (iTasks == 0)
return;
var iStartTime:int = getTimer();
// Loop while there is time left.
while (!((getTimer() - iStartTime) < THREAD_ALLOTMENT)) {
// Start with the last task.
var tTask:IPseudoThreadTask = mtTasks.get(iTasks-1) as IPseudoThreadTask;
var bComplete:Boolean = tTask.run(this);
// Note that we need to remove the specific task in some way, we can't
// just pop the task off the end because other tasks may have been added.
if (bComplete)
mtTasks.removeIndex(iTasks-1);
}
}
}
When I originally created our version of the Pseudo-thread manager, it actually operated independently of most of the codebase through the use of the callLater() method, which would call back into the PsuedoThreadManager every frame. I never found out why, but in doing so this would interrupt execution of other parts of the codebase that were listening for ENTER_FRAME events when interacting with other UIComponents. Instead, I just had our main update thread call the pseudo-thread manager’s run() method.
Implementing the Hierarchical Task
Now that we have a task manager, a task interface, and an operation we want to break up, we just need to implement our hierarchical operation as a task.
public class XmlHierarchyTask implements IPseudoThreadTask {
/**
* The root node being operated on.
*/
private var mtRoot:IMutableXmlNode;
/**
* The action operating on the hierarchy.
*/
private var mtAction:IXmlHierarchyAction;
/**
* Data members for the current state of the task.
*/
private var miChildIndex:int = 0;
private var miChildren:int = 0;
private var mtChildren:IObjectArray;
/**
* Constructor.
*/
public function XmlHierarchyTask(tRoot:IMutableXmlNode, tAction:IXmlHierarchyAction) {
mtRoot = tRoot;
mtAction = tAction;
mtChildren = tRoot.getChildren();
if (mtChildren != null) {
miChildIndex = mtChildren.size();
}
}
/**
* Run the task.
* @return whether or not the task is finished.
*/
public function run(tManager:PseudoThreadManager):Boolean {
var bReturn:Boolean = false;
try {
mtAction.nodeEncountered(null, mtRoot);
}
catch (tEx:Error) {
// handle error
}
// We will only process a single child at a time. This is
// effectively a depth-first processing of the hierarchy.
if (miChildIndex < miChildren) {
var tCurrentChild:IMutableXmlNode = IMutableXmlNode(mtChildren.get(miChildIndex));
tManager.addTask(new XmlHierarchyTask(tCurrentChild, mtAction));
++miChildIndex;
}
if (miChildIndex == miChildren) {
bReturn = true;
}
return bReturn;
}
}
Take some time to compare the XmlHierarchyTask with the original hierarchy operation that we started with. Notice that for the most part the same structure is there: we invoke the operation on the specific node and loop over the children. The only difference is that the children are being operated on through successive calls to the run() method. Once the final child has been operated on, the task can return true so that it will be removed from the PseudoThreadManager.
Breaking up the Action
So what if the action that you’re using to operate on the hierarchy takes a long time? Well, you already have an interface for the task, so just implement the action itself as a task. Here’s a quick example of how an action that has three separable operations might be implemented:
public class XmlHierarchyAction implements IPseudoThreadTask {
/**
* The node the action is operating on.
*/
private var mtNode:IXmlNode;
/**
* State variables.
*/
private var mbCompletedFirstPart:Boolean = false;
private var mbCompletedSecondPart:Boolean = false;
/**
* Constuctor.
*/
public function XmlHierarchyAction(tNode:IXmlNode) {
mtNode = tNode;
}
/**
* Run the task.
* @return whether or not the task is finished.
*/
public function run(tManager:PseudoThreadManager):Boolean {
var bComplete:Boolean = false;
if (!mbCompletedFirstPart) {
// Do the first part of the action
mbCompletedFirstPart = true;
}
else if (!mbCompletedSecondPart) {
// Do the first part of the action
mbCompletedSecondPart = true;
}
else {
// Do the third part of the operation.
bComplete = true;
}
return bComplete;
}
}
The XmlHierarchyAction could just create one of these instead of calling nodeEncountered() as it did before. The pseudo-thread manager is unused here, but again, if this action spawned other tasks, it would be required.
Wrap Up
So that’s all we really use to manage our long running operations. It’s not a lot of code, but getting into the actual implementations of some of the actions can get confusing. Refactoring that functionality to work in a pseudo-threaded environment can be difficult for a lot of reasons. You may need to use listeners to know when a particular task has been completed, for example. We do this in Sharendipity so that we know when an entire Application has been initialized, displaying a spinner or progress bar that is updated while the initialization is happening.
I hope that this gives you an idea of how to break up operations in ActionScript. There may be other solutions out there but I wasn’t able to find them. If you have any suggestions, please leave them in the comments.
Tags:Action, ActionScript, chunking, Flash, Hierarchy, multi-threading, pseudo-threading, Task, thread