The last article gave a very basic example of the flash.concurrent.Condition class introduced in Flash Player 11.5. That example was (hopefully) a simple and easy way to understand the mechanics of how the Condition class works. Unfortunately, it was not a useful example and actually demonstrated the opposite of what you’d want to use it for. Today’s article shows a somewhat more complicated example that should serve as an example of appropriate usage for Condition.

If you’ve never used Condition before or aren’t used to its equivalents in other languages, it can be somewhat difficult to understand at first. On top of that, the official documentation is rather sparse and includes no code examples. This article and its predecessor hope to help.

The first thing to remember is that Condition wraps up a Mutex and provides some convenience functionality. In order to call notify(), notifyAll(), or wait() on it, the worker you’re calling from needs to have locked that Mutex. This often means that you’ll call lock() on the Mutex before one of these operations. For more on the mechanics, see the previous article.

The following demo uses a worker thread to compress a big ByteArray. This takes a long time and ByteArray.compress() blocks until the whole compression is complete. Since we don’t want to block the main worker thread from updating the UI, taking in user input, and so forth, we’ll use another worker thread to do the compression while the main worker thread continues to update.

For simplicity’s sake, this demo simply shows a button that you can use to start the compression. A simple TextField updates every frame while the compression takes place to let you know that the UI is still updating. If you click the button to start the compression while the compression is already occurring, an error will be displayed. This lets you know that the app can still take in user input. When compression is complete, a simple error message with some stats is displayed and you can perform more compressions.

package
{
	import flash.concurrent.Mutex;
	import flash.concurrent.Condition;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.text.TextFormat;
	import flash.system.MessageChannel;
	import flash.system.Worker;
	import flash.system.WorkerDomain;
	import flash.utils.ByteArray;
	import flash.utils.getTimer;
 
	/**
	* Demo of uses for the Condition class. Uses a worker thread to compress
	* a big ByteArray without blocking the main (UI) worker thread.
	* @author Jackson Dunstan (JacksonDunstan.com)
	*/
	public class ConditionDemo extends Sprite
	{
		/** 128 megabytes */
		private static const SIZE:uint = 1024*1024*128;
 
		/** Logger for the status of the compression */
		private var logger:TextField = new TextField();
 
		/** Error message display */
		private var errorMessage:TextField = new TextField();
 
		/** Worker that compresses for the main worker */
		private var worker:Worker;
 
		/** Bytes to compress */
		private var bytes:ByteArray;
 
		/** Condition the worker is waiting on to start the compression */
		private var condition:Condition;
 
		/** Message channel from the worker thread to the main thread */
		private var workerToMain:MessageChannel;
 
		/**
		* Start the app in main thread or worker thread mode
		*/
		public function ConditionDemo()
		{
			// If this is the main SWF, start the main thread
			if (Worker.current.isPrimordial)
			{
				startMainThread();
			}
			// If this is the worker thread SWF, start the worker thread
			else
			{
				startWorkerThread();
			}
		}
 
		/**
		* Start the main thread
		*/
		private function startMainThread(): void
		{
			// Create the worker from our own SWF bytes
			var swfBytes:ByteArray = this.loaderInfo.bytes;
			worker = WorkerDomain.current.createWorker(swfBytes);
 
			// Create a message channel to receive from the worker thread
			workerToMain = worker.createMessageChannel(Worker.current);
			workerToMain.addEventListener(Event.CHANNEL_MESSAGE, onWorkerToMain);
			worker.setSharedProperty("workerToMain", workerToMain);
 
			// Start the worker
			worker.start();
 
			// Make a button to start the compressing
			makeButton("Compress", onCompressButtonClicked);
 
			// Show a logger
			logger.y = height + 10;
			logger.autoSize = TextFieldAutoSize.LEFT;
			logger.defaultTextFormat = new TextFormat("_sans", 11);
			addChild(logger);
 
			// Show an error message
			errorMessage.x = width + 10;
			errorMessage.autoSize = TextFieldAutoSize.LEFT;
			errorMessage.defaultTextFormat = new TextFormat("_sans", 11, 0xff0000);
			addChild(errorMessage);
		}
 
		/**
		* Start the worker thread
		*/
		private function startWorkerThread(): void
		{
			// Create a condition indicating that we're waiting for bytes to
			// compress
			condition = new Condition(new Mutex());
			Worker.current.setSharedProperty("condition", condition);
 
			// Get the message channel for notifying the main worker thread that
			// we're done compressing
			workerToMain = Worker.current.getSharedProperty("workerToMain");
 
			// Endlessly loop compressing bytes that the main worker thread
			// gives us
			while (true)
			{
				// Lock the condition mutex so the worker thread can wait on
				// the condition. This pauses the worker thread until the mutex
				// can be locked.
				condition.mutex.lock();
 
				// Wait for the bytes to be ready for compression. This releases
				// the condition's mutex and pauses the worker thread until the
				// main worker thread calls notify() on the condition.
				condition.wait();
 
				// Get the bytes to compress
				bytes = Worker.current.getSharedProperty("bytes");
 
				// Compress the bytes
				bytes.compress();
 
				// Notify the main thread that the bytes have been compressed
				workerToMain.send(true);
			}
		}
 
		private function onCompressButtonClicked(ev:Event): void
		{
			// Get the condition the worker created for us
			condition = worker.getSharedProperty("condition");
 
			// Try to take ownership of the condition mutex
			if (condition.mutex.tryLock())
			{
				// Create a big ByteArray for the worker to compress
				bytes = new ByteArray();
				bytes.length = SIZE;
				bytes.shareable = true;
				worker.setSharedProperty("bytes", bytes);
 
				// Clear the status text displays
				errorMessage.text = "";
				logger.text = "";
 
				// The worker is waiting, so we got the lock. Notify the worker
				// that the bytes are ready and release our ownership of the
				// mutex since we only needed it to notify.
				condition.notify();
				condition.mutex.unlock();
 
				// Check if the worker is done every frame. This demonstrates
				// that the main worker thread is not blocked while the worker
				// thread compresses the bytes.
				addEventListener(Event.ENTER_FRAME, onEnterFrame);
			}
			else
			{
				// The worker is not waiting, so show an error message
				// indicating that it's too busy for a new request
				errorMessage.text = "Error: worker busy at " + getTimer();
			}
		}
 
		private function onEnterFrame(ev:Event): void
		{
			// We did not get the mutex. This indicates that the compression
			// is not yet complete. Show a message indicating that.
			logger.text = "Compressing. Time: " + getTimer();
		}
 
		/**
		* Callback for when the worker thread sends a message to the main thread
		* via a MessageChannel
		* @param ev CHANNEL_MESSAGE event
		*/
		private function onWorkerToMain(ev:Event): void
		{
			// The message indicates that the compression is complete, so stop
			// checking every frame.
			removeEventListener(Event.ENTER_FRAME, onEnterFrame);
 
			// Output some statistics about the work that was done
			logger.text= "Done compressing " + SIZE + " bytes to "
				+ bytes.length + " bytes ("
				+ ((bytes.length*100.0)/SIZE).toFixed(2) + "%)";
		}
 
		private function makeButton(label:String, callback:Function): void
		{
			const PAD:Number = 3;
			const TEXT_FORMAT:TextFormat = new TextFormat("_sans", 11);
 
			var tf:TextField = new TextField();			
			tf.defaultTextFormat = TEXT_FORMAT;
			tf.name = "label";
			tf.text = label;
			tf.autoSize = TextFieldAutoSize.LEFT;
			tf.selectable = false;
			tf.x = tf.y = PAD;
 
			var button:Sprite = new Sprite();
			button.name = label;
			button.graphics.beginFill(0xE6E2D1);
			button.graphics.drawRect(0, 0, tf.width+PAD*2, tf.height+PAD*2);
			button.graphics.endFill();
			button.graphics.lineStyle(1, 0x000000);
			button.graphics.drawRect(0, 0, tf.width+PAD*2, tf.height+PAD*2);
			button.addChild(tf);
			button.addEventListener(MouseEvent.CLICK, callback);
 
			button.x = PAD + this.width;
			button.y = PAD;
			addChild(button);
		}
	}
}

Launch the demo

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