Threads in AS3, Flex, Actionscript: Flash, AS3, Flex multi-threading

[Updated 10/27/2009: The PseudoThread framework described below has been incorporated into the AS3-commons project: Concurrency Module. You can check out the latest source by clicking here or read an updated HOWTO located here.]

Ok, those of you who know AS3 and the Flash Player are saying “Wait a second, there is no threading in AS3″, and you are correct, however for those of you from a heavy Java background like myself, I’m here to provide you with a sample of code that will give you some pseudo threading capability that you are accustomed to.

Why would you need something like this? Well since the player processes applications with one thread, if you need to do some sort of background operation that is intensive, doing so may freeze up the GUI which would prevent the user from being able to do anything until the task completes. By using a thread like PseudoThread you can prevent the application from freezing while your background operation runs over time.

I’ve provided two classes, PseudoThread and IRunnable as well as a simple example (main.mxml) which uses this little framework. One could create a more robust and larger AS3 threading framework based on the concepts of PseudoThread and IRunnable. One might want to introduce a thread manager, the abilities to stop() and pause() as well as throttle the rate of processing. Either way, if you need threading capability, these classes below should be of help to you and at a minimum help to get you going on your own custom implementation.

To get started checkout the source over at the bitsofinfo project at Google Code or you can get it from the AS3 Commons – Concurrency Module

The overall concept here is that a PseudoThread processes an IRunnable until the IRunnable is completed with it’s work or until an optional timeout is reached. You implement the details of an IRunnable however you see fit. An IRunnable is responsible for doing some task within it’s process() method, and this method will be called again and again until your IRunnable’s isComplete() method return’s true and then the PseudoThread will stop.

IRunnable

Lets first take a look at IRunnable (below) which is the interface that your code implements in order to be processed by a PseudoThread. This is similar to a Runnable in Java. The getTotal() and getProgress() are methods which would permit some abstract external resource to display or report/track progress of an existing unit of work being processed by an PseudoThread. PseudoThread itself utilizes these methods as it dispatches ProgressEvents.

package org.bitsofinfo.thread {
	
	/**
	 * An IRunnable is to be used with PseudoThread which
	 * will call an IRunnable's process() method repeatedly
	 * until a timeout is reached or the isComplete() method returns
	 * true.
	 * 
	 * @see PseudoThread
	 * 
	 * */
	public interface IRunnable {
		
		/**
		 * Called repeatedly by PseudoThread until
		 * a timeout is reached or isComplete() returns true.
		 * Implementors should implement their functioning
		 * code that does actual work within this method
		 * 
		 * */
		function process():void;
		
		/**
		 * Called by PseudoThread after each successful call
		 * to process(). Once this returns true, the thread will
		 * stop.
		 *
		 * @return	boolean	true/false if the work is done and no further
		 * 			calls to process() should be made
		 * */
		function isComplete():Boolean;
		
		/**
		 * Returns an int which represents the total
		 * amount of "work" to be done.
		 * 
		 * @return	int	total amount of work to be done
		 * */
		function getTotal():int;
		
		/**
		 * Returns an int which represents the total amount
		 * of work processed so far out of the overall total
		 * returned by getTotal()
		 * 
		 * @return	int	total amount of work processed so far
		 * */
		function getProgress():int;
		
		
	}
	
}

PseudoThread

Next is PseudoThread which is the actual workhorse. To process an IRunnable you simply create a PseudoThread and pass it an IRunnable who’s process() method will be continually called by the PseudoThread every msDelay milliseconds as specified via PseudoThread’s constructor. PseudoThread is really nothing more than a generic wrapper of the flash Timer class. The PseudoThread dispatches events for complete, error and progress.

package org.bitsofinfo.thread {
	
	import flash.events.ErrorEvent;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.ProgressEvent;
	import flash.events.TimerEvent;
	import flash.utils.Timer;
	
	/**
	 * Dispatched when the thread's work is done
	 * 
	 * @eventType	flash.events.Event.COMPLETE
	 */ 
	[Event(name="complete", type="flash.events.Event")]     
	
	/**
	 * Dispatched when the thread encounters an error
	 * or if the Thread times out
	 * 
	 * @eventType	flash.events.ErrorEvent.ERROR
	 */ 
	[Event(name="error", type="flash.events.ErrorEvent")]
	
	/**
	 * Dispatched when the thread's work makes progress
	 * 
	 * @eventType	flash.events.ErrorEvent.ERROR
	 */ 
	[Event(name="progress", type="flash.events.ProgressEvent")]    
	
	/**
	 * <p>This class simulates a thread in ActionScript </p>
	 * 
	 * <p>You create a PsuedoThread by passing an IRunnable
	 * who's process() function will be called every "msDelay" milliseconds.
	 * The IRunnable's isComplete() method is consulted
	 * after each process() invocation to determine if the processing
	 * should cease. When IRunnable.isComplete() returns true
	 * the Thread will terminate and fire off the Event.COMPLETE
	 * event. </p>
	 * 
	 * <p>PseudoThreads are useful for time consuming processing operations
	 * where a delay in the UI is un-acceptable. The smaller you set the <code>msDelay</code>
	 * setting, the faster this thread will execute and subsequently other parts of your application
	 * will be less responsive (noteably a GUI).</p>
	 * 
	 * <p>Note! To prevent memory leaks in your application callers must always remember 
	 * to de-register for the complete event from this thread after it has been received
	 *  (as well as the progress and error events!)</p>
	 * 
	 * <P>Caller can also specify the max amount of time this Thread should run before it will 
	 * 	stop processing and throw an Error. This is done via the <code>msTimeout</code> constructor
	 * argument. If no timeout is specified the process will run forever.</P>
	 * 
	 * 
	 * */
	public class PseudoThread extends EventDispatcher {
		
		// the timer which is the core of our PseudoThread
		private var intTimer:Timer = null;
		
		// total times we have ran
		private var totalTimesRan:int = 0;
		
		// default max runtimes is forever
		private var maxRunTimes:int = 0;
		
		// the IRunnable we are processing
		private var runnable:IRunnable = null;
		
		// a unique name for me
		private var myName:String;

			
		
		/**
		 * Constructor. 
		 * 
		 * @param	runnable			The IRunnable that this thread will process. The IRunnable's process()
		 * 								method will be called repeatably by this thread.
		 * 
		 * @param	threadName			a name identifier
		 * 
		 * @param	msDelay				delay between each thread "execution" call of IRunnable.process(), in milliseconds
		 * 
		 * @param	msTimeout			the max amount of time this PseudoThread should run before it will 
		 * 								stop processing and dispatch an ErrorEvent. If no timeout is specified
		 * 								the process will run until the IRunnable reports that it is complete.
		 * 
		 * */
		public function PseudoThread(runnable:IRunnable, threadName:String, msDelay:Number=200, msTimeout:Number=-1) {
			this.myName = threadName;
			this.runnable = runnable;

			if (msTimeout != -1) {
				if (msTimeout < msDelay) {
					throw new Error("PseudoThread cannot be constructed with a timeoutMS that is less than the delayMS");
				}
				maxRunTimes = Math.ceil(msTimeout / msDelay);
			}

			this.intTimer = new Timer(msDelay,maxRunTimes);
			this.intTimer.addEventListener(TimerEvent.TIMER,processor);
		}
		

		
		/** 
		 * Destroys this and deregisters from the Timer event
		 * */
		public function destroy():void {
			this.intTimer.stop();
			this.intTimer.removeEventListener(TimerEvent.TIMER,processor);
			this.runnable = null;
			this.intTimer = null;
		}

		/**
		 * Called each time our internal Timer executes. Here we call the runnable's process() function 
		 * and then check the IRunnable's state to see if we are done. If we are done we dispatch a complete
		 * event. If progress is made we dispatch progress, lastly on error, this will destroy itself 
		 * and dispatch an ErrorEvent.<BR><BR>
		 * 
		 * Note that an ErrorEvent will be thrown if a timeout was specified and we have reached it without
		 * the IRunnable reporting isComplete() within the timeout period.
		 * 
		 * @throws ErrorEvent when the process() method encounters an error or if the timeout is reached.
		 * @param	e TimerEvent
		 * */
		private function processor(e:TimerEvent):void {
			try {
				this.runnable.process();
				this.totalTimesRan++;
				
			} catch(e:Error) {
				destroy();
				this.dispatchEvent(new ErrorEvent(ErrorEvent.ERROR,false,false,"PsuedoThread ["+this.myName+"] encountered an error while" + 
						" calling the IRunnable.process() method: " +e.message));
				return;
			}
			
			if (runnable.isComplete()) {
				this.dispatchEvent(new Event(Event.COMPLETE,false,false));
				destroy();
			} else {
				
				if (this.maxRunTimes != 0 && this.maxRunTimes == this.totalTimesRan) {
					destroy();
					this.dispatchEvent(new ErrorEvent(ErrorEvent.ERROR,false,false,"PsuedoThread ["+this.myName+"] " + 
							"timeout exceeded before IRunnable reported complete"));
					return;
					
				} else {
					this.dispatchEvent(new ProgressEvent(ProgressEvent.PROGRESS,false,false,runnable.getProgress(),runnable.getTotal()));
				}
			}
		}

		/**
		 * This method should be called when the thread is to start running and calling
		 * it's IRunnable's process() method until work is finished.
		 * 
		 * */
		public function start():void {
			this.intTimer.start(); 
		}

	}
}

TestRunnable

TestRunnable is an example of a very basic IRunnable implementation. TestRunnable’s process method simply increments a counter each time it is called by the PseudoThread. Once it gets to 100, the unit of work is complete. Again this is a very simple example but you could implement an IRunnable who’s process() method does considerably more complex work.

package org.bitsofinfo.thread
{

	public class TestRunnable implements IRunnable
	{
		
		private var counter:int = 0;
		private var totalToReach:int = 100;
		
		public function TestRunnable() {
			
		}

		public function process():void {
			counter++;
		}
		
		public function isComplete():Boolean {
			return counter == totalToReach;
		}
		
		public function getTotal():int
		{
			return this.totalToReach;
		}
		
		public function getProgress():int
		{
			return this.counter;
		}
		
	}
}

main.mxml

main.mxml brings it all together and runs a TestRunnable via a PseudoThread and displays the progress on the screen as well as allowing you to restart/start the simple test.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="456" height="76"
	 creationComplete="reset()" viewSourceURL="srcview/index.html">

	<mx:Script>
		<![CDATA[
			import org.bitsofinfo.thread.TestRunnable;
			import org.bitsofinfo.thread.PseudoThread;
			import org.bitsofinfo.thread.IRunnable;
			
			var runnable:IRunnable;
			var thread:PseudoThread;
			
			private function createThread():void {
				if (thread != null) {
					try {
						thread.destroy();
					} catch(e:Error) {
						// thread might already be destroyed due to actually being finished vs pressing reset in the middle
					}
				}
				
				thread = new PseudoThread(createRunnable(),"Test",100);
			}
			
			private function createRunnable():IRunnable {
				runnable = new TestRunnable();
				return runnable;
			}
			
			private function start():void {
				thread.addEventListener(Event.COMPLETE,handleComplete);
				thread.addEventListener(ProgressEvent.PROGRESS,handleProgress);
				thread.start();
			}
			
			private function handleComplete(e:Event):void {
				thread.removeEventListener(Event.COMPLETE,handleComplete);
				thread.removeEventListener(ProgressEvent.PROGRESS,handleProgress);
				this.progBar.setProgress(100,100);
				this.progress.text = runnable.getTotal().toString();
				progBar.label= "CurrentProgress" + " 100%";
			}
			
			private function handleProgress(e:ProgressEvent):void {
				this.progBar.setProgress(e.bytesLoaded,e.bytesTotal);
				this.progress.text = e.bytesLoaded.toString();
				progBar.label= "CurrentProgress" + " " + Math.ceil(e.bytesLoaded/e.bytesTotal *100) + "%";
				
				
			}
			
			private function reset():void {
				if (thread != null) {
					thread.removeEventListener(Event.COMPLETE,handleComplete);
					thread.removeEventListener(ProgressEvent.PROGRESS,handleProgress);
				}
				
				this.progBar.setProgress(0,100);
				this.progBar.label =  "CurrentProgress" + " 0%";
				
				createThread();
				this.progress.text= this.runnable.getProgress().toString();
				this.totalWork.text = this.runnable.getTotal().toString();
			}
			
		]]>
	</mx:Script>	

	<mx:Label x="10" y="14" text="Total work:"/>
	<mx:Label x="125" y="14" text="Progress:"/>
	<mx:Label x="83" y="14" width="30" id="totalWork" visible="true"/>
	<mx:Label x="184" y="13" width="39" id="progress" />
	<mx:Button x="249" y="11" label="Start" width="67" click="start()"/>
	<mx:Button x="324" y="11" label="Reset" width="67" click="reset()"/>
	<mx:ProgressBar id="progBar" x="10" y="40" width="425"
		minimum="0" visible="true" maximum="100" label="CurrentProgress 0%" 
            direction="right" mode="manual" />
	
</mx:Application>

Hopefully this post was of some help to those of you who have a need for threading like behavior in ActionScript, Flex or basic AS3. Again, this post was recently updated and now you can checkout the source for the PseudoThread at Google Code.

I also apologize for not being able to just show the working example directly here in this post. WordPress.com does not permit embeds!

About these ads
Tagged , ,

2 thoughts on “Threads in AS3, Flex, Actionscript: Flash, AS3, Flex multi-threading

  1. tiana says:

    thanks, i will try

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 26 other followers

%d bloggers like this: