Pages

Google+

Wednesday, January 13, 2010

Synchronization Techniques for Flash & AS3: Part I - The Semaphore

[Edit] The source code for this has been updated and can be seen and downloaded on this follow up post.
Flash does not support multihreading, and that's probably good thing since managing threads can be very, very difficult; however, Flash can have a kind of concurrency when it comes to user interface, multi-user applications (e.g. games) and synchronizing animations. With user interfaces, a developer may wish to disable parts or all of the UI while asynchronous events, such as loading assets and managing various animations are executing. Within a multi-user environment, imagine a game where players share a limited amount of resources - checking things in and out as they become available ( see 'Dining Philosophers Problem').  Another scenario might be the need to manage many, many animations.

When it comes to concurrency there are various techniques that come to mind - The Semaphore , Java's ReentrantLock and CountDownLatch, the Actor model, and Scala's Mailbox. This first post will focus on the semaphore.




Implementing Semaphores With AS3


There are a couple AS3 semaphores out there (one, two), but as always I enjoy coming up with my own and then compare it with others. This guy does a good job of explaining the usefulness of semaphores, so this post will focus on implementing them. Here is an example of a simple semaphore in action.



A 'Synchronous' Semaphore


[SWF]http://blog.alanklement.com/files/examples/semaphores/Synchronous_Semaphore.swf, 645, 200[/SWF]


The above example uses a very simple semaphore.  There is one instance of a semaphore object.  Every ball has a reference to that semaphore, and when each ball starts to animate, it adds a lock (which in turn, increments a counter by one) to the semaphore and when it finishes it releases the lock ( decrementing the counter by one). When all the the semaphore's locks are released, it executes a callback. In this example I have the callback re enable my "Start Animations" button back in.


note: any 'random...' functions are package level functions. I did this to keep my demo classes focused on the semaphore and animations.



// Sync_Semaphore_Demo.as
private function create_semaphore() : void
{
//enable_start_btn is my callback function that enables the start button
ball_animation_sync = new Simple_Semaphore(enable_start_btn);
}

private function start_animations(event : MouseEvent) : void
{
disable_start_btn();

for (var i : int = 0;i < num_of_balls;i++)
{
Sync_Animated_Object(balls[i]).start_animation(random_point(), random_time(), ball_animation_sync);
}
}

Above, I create my semaphore, passing it my complete callback. Then all of the balls are told to start animating together. Sync_Animated_Object is an interface with one method - it's signature shown below:
//Sync_Animated_Object.as
package display
{
import semaphore.Semaphore;
import flash.geom.Point;

public interface Sync_Animated_Object extends Display_Object, Disposable_Object
{
function start_animation(end_pos:Point, animation_length:Number,animation_sync:Semaphore) : Boolean;
}
}

Here is how the ball implements this interface:
// Ball.as
public function start_animation(end_pos : Point, animation_length : Number,animation_sync : Semaphore) : Boolean
{
if(animation_sync.aquire_lock())
{
this.animation_sync = animation_sync;
animation_sync.aquire_lock();
var random_size : Number = random_number_range(.1,1.5);
// Note this Tweenlite object's 'onComplete' property
TweenLite.to(ball, animation_length, {x:end_pos.x, y:end_pos.y, alpha:1, onComplete:release_lock,scaleX:random_size,scaleY:random_size, ease:Elastic.easeOut});
return true;
}
else
{
return false;
}
}
// When the animation is finished it will release the lock
private function release_lock() : void
{
TweenLite.to(ball, .25, {alpha:.25});
animation_sync.release_lock();
animation_sync = null;
animation_length = NaN;
}

My start_animation method attempts to acquire a lock, if it cannot secure a lock it will return false. As this is just my Simple_Semaphore, it will always return true because I have set no limit as to how many locks it can acquire. My semaphore will simply wait until it is told that all locks are released (when each ball has finished it's animation and unlocks the semaphore).

Below is my Simple_Semaphore:
package semaphore
{
public class Simple_Semaphore implements Semaphore
{
private var lock_count : uint = 0;
private var all_locks_open_callback : Function;
private var locks_initiated : Boolean = false;

public function Simple_Semaphore(unlocked_callback : Function)
{
this.all_locks_open_callback = unlocked_callback;
}

public function aquire_lock() : Boolean
{
lock_count++;
locks_initiated = true;
return true;
}

public function release_lock() : void
{
lock_count--;
if(lock_count

Just my style: simple.



An 'Asynchronous' Semaphore


[SWF]http://blog.alanklement.com/files/examples/semaphores/Asynchronous_Semaphore.swf, 645, 200[/SWF]

The above is an example of the same Simple_Semaphore object above, but done in an asynchronous fashion. Whenever an animation is taking place, the 'Reset All Animations' button is disabled; therefore, as long as the user consistently clicks on the 'Add Animation' button, 'Reset All Animations' will always be disabled. All code is the same except for add_animation method and an additional method reset_animations. Here is the noteworthy code below.
//Async_Semaphore_Demo.as
private function add_animation(event : MouseEvent) : void
{
disable_reset_btn();

var ball : Ball = new Ball(random_size(), random_color(), random_point());
ball.display_object.alpha = 0;
// remember that 'ball_animation_sync' is my semaphore instance.
ball.start_animation(random_point(), random_time() + 1, ball_animation_sync);

balls.push(ball);
ball_canvas.addChild(ball.display_object);
}

private function reset_animations(event : MouseEvent) : void
{
for (var i : int = 0;i < balls.length;i++)
{
var ball : Sync_Animated_Object = Sync_Animated_Object(balls[i]);
ball_canvas.removeChild(ball.display_object);
ball.dispose_for_garbage_collection();
ball = null;
}

balls.length = 0;
disable_reset_btn();
}

A 'Counting' or 'Permit' Semaphore


The 'counting' or 'permit' semaphore is perhaps the most familiar to developers. This is likely due to the Java implementation of the Semaphore.  For these, there is a set number of times the semaphore can be locked. This is useful to control resources of various kinds. If to many animations are executing at once, the semaphore will execute a callback notifying when the max is hit, as well as when a permit is available. Example below:
[SWF]http://blog.alanklement.com/files/examples/semaphores/Permit_Semaphore.swf, 645, 200[/SWF]

Pretty simple as well.  I named this concrete class Standard_Semaphore. It extends Simple_Semaphore and implements the Permit_Semaphore interface.
// Permit_Semaphore.as
package semaphore
{
public interface Permit_Semaphore extends Semaphore
{
function get num_of_permits_available() : uint;

function set permit_available_callback(callback:Function):void;

function drain_all_permits() : void;

function reduce_permits(num_permits_to_remove:uint) : void;

function increment_permits(num_permits_to_add:uint) : void;
}
}

And a peek at Standard_Semaphore:
// Simple_Semaphore.as
package semaphore
{
public class Standard_Semaphore extends Simple_Semaphore implements Permit_Semaphore
{
private var max_permits : uint;
private var num_permits_given_out : uint;
private var notify_permit_available : Function;

public function Standard_Semaphore(unlocked_callback : Function, max_permits : uint, permit_available_callback:Function = null)
{
super(unlocked_callback);
this.max_permits = max_permits;
this.notify_permit_available = permit_available_callback;
}

override public function aquire_lock() : Boolean
{
if(this.num_of_permits_available != 0)
{
super.aquire_lock();
num_permits_given_out++;
return true;
}
else
{
return false;
}
}

override public function release_lock() : void
{
super.release_lock();
num_permits_given_out--;

if(notify_permit_available != null)
{
notify_permit_available();
}
}

override public function dispose_for_garbage_collection() : void
{
super.dispose_for_garbage_collection();
max_permits = NaN;
num_permits_given_out = NaN;
notify_permit_available = null;
}

public function get num_of_permits_available() : uint
{
return max_permits - num_permits_given_out;
}

public function set permit_available_callback(callback : Function) : void
{
this.notify_permit_available = callback;
}

public function drain_all_permits() : void
{
num_permits_given_out = max_permits;
}

public function reduce_permits(num_permits_to_remove : uint) : void
{
max_permits - num_permits_to_remove;
}

public function increment_permits(num_permits_to_add : uint) : void
{
max_permits += num_permits_to_add;
}
}
}

Download the project complete with all source files and launcher configurations. If you have FDT, this zip file can be imported directly as a project and you will ready to go.

Final Thoughts


This method also completely avoids Flash's event system. This leads to a faster, more reliable and cleaner implementation. Just imagine the performance hit there would be if there were 100 balls on the screen and each was dispatching events, and then subsequently having to catch and delete those pointers. Goodness. I also make an effort to use interfaces and subclass appropriately, hoping to express my intent for these classes.

My use of the terms 'asynchronous' and 'synchronous' may not be accurate, but I think it explains the behavior well.

As always, comments and critiques are welcome and appreciated.

This code uses Keith Peter's Minimal Comps and Jack Doyle's Tweenlite. Thanks to those guys for their hard work.