[UPDATE] Adobe has removed support for ByteArray.shareable until Player 11.5. The source code has been updated to work around that restriction, but have left the example code intact since it will eventually work fine.
In Part 1 of my Intro to AS3 Workers series I looked at the fundamentals of AS3 workers, including the various communication methods, and showed an example of a simple Hello World worker.
In this next post, I’ll take it a step further and show how you can start doing something useful, like image processing! In this case, I’ll be applying a Sharpen Filter to a large bitmap, while the main UI thread continues to render at 30fps.
DEMO: Single-Threaded
Here you can take a look at what we’re going to make. This version does not use workers, and you can see that the slider locks up completely as the image is processed:
DEMO: Multi-Threaded
Here is the same demo, but using a Worker. You can see the UI renders at a smooth 30fps:
The Code
Before actually jumping into the code, it’s important to plan ahead. Especially when dealing with concurrency, we really need to create an efficient system for marshaling data between the workers, otherwise your main thread will begin to bog down due to the heavy cost of serializing and de-serializing data.
After a bit of a thought, I decided to architect the app like so:
- The bitmapData would be shared with the worker using a shareable byteArray
- We will use the bitmapData.setPixels(), and bitmapData.copyPixelsToByteArray() API’s to convert the bitmap > byteArray, and vice versa.
- The main thread will issue “SHARPEN” commands to the worker, and the worker will send “SHARPEN_COMPLETE” when it’s done
- The worker will use a 500ms timer to check whether it needs to run a new Sharpen operation. This prevents excessive Sharpen operations.
The Document Class
First up is the constructor, here we’ll use the same loaderInfo.bytes trick as we did in the last tutorial. This constructor is run twice, the second instance is the worker, and it creates a SharpenWorkerinstance which will handle all communication back to the main thread.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 | public class ImageWorkerExample extends Sprite { ... public function ImageWorkerExample(){ //Main thread if(Worker.current.isPrimordial){ initUi(); initWorker(); } //If not the main thread, we're the worker else { sharpenWorker = new SharpenWorker(); } } |
Lets continue to walk through the main class before looking at Sharpen Worker.
The call to initUi() simply creates a slider, and an image, and adds them to the stage. No need to look at that here.
The next function is initWorker(), take a look at that with comments inlined:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | protected function initWorker():void { //Create worker from the main swf worker = WorkerDomain.current.createWorker(loaderInfo.bytes); //Create message channel TO the worker mainToBack = Worker.current.createMessageChannel(worker); //Create message channel FROM the worker, add a listener. backToMain = worker.createMessageChannel(Worker.current); backToMain.addEventListener(Event.CHANNEL_MESSAGE, onBackToMain, false, 0, true); //Now that we have our two channels, inject them into the worker as shared properties. //This way, the worker can see them on the other side. worker.setSharedProperty("backToMain", backToMain); worker.setSharedProperty("mainToBack", mainToBack); //Init worker with width/height of image worker.setSharedProperty("imageWidth", origImage.width); worker.setSharedProperty("imageHeight", origImage.height); //Convert image data to (shareable) byteArray, and share it with the worker. imageBytes = new ByteArray(); imageBytes.shareable = true; origImage.copyPixelsToByteArray(origImage.rect, imageBytes); worker.setSharedProperty("imageBytes", imageBytes); //Lastly, start the worker. worker.start(); } |
In the above code, we did a few things:
- Created a worker using our own loaderInfo.bytes
- Created message channels so that we can talk to the Worker, and vice-versa
- Copied the image data into a shareable byteArray
- ‘Shared’ the image data with the worker
- Started the worker
The next step in the main class, is to ask the worker to sharpen something, and to respond when the worker has completed the task.
First, we’ll wire up the Slider CHANGE handler:
0 1 2 3 4 5 | protected function onSliderChanged(value:Number):void { //Send the sharpen command to our worker. mainToBack.send("SHARPEN"); mainToBack.send(value); } |
Next, we’ll handle the completion from the worker:
0 1 2 3 4 5 6 7 | protected function onBackToMain(event:Event):void { var msg:String = backToMain.receive(); if(msg == "SHARPEN_COMPLETE"){ imageBytes.position = 0; image.bitmapData.setPixels(image.bitmapData.rect, imageBytes); } } |
That is it for the main class!
The Worker
The constructor for the worker is fairly straight forward:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public class SharpenWorker extends Sprite { ... public function SharpenWorker(){ //Get reference to current worker (ourself) var worker:Worker = Worker.current; //Listen on mainToBack for SHARPEN events mainToBack = worker.getSharedProperty("mainToBack"); mainToBack.addEventListener(Event.CHANNEL_MESSAGE, onMainToBack); //Use backToMain to dispatch SHARPEN_COMPLETE backToMain = worker.getSharedProperty("backToMain"); //Get the image data from the shareProperty pool imageBytes = worker.getSharedProperty("imageBytes"); var w:int = worker.getSharedProperty("imageWidth"); var h:int = worker.getSharedProperty("imageHeight"); imageBytes.position = 0; imageData = new BitmapData(w, h, false, 0x0); backToMain.send(imageBytes.length); imageData.setPixels(imageData.rect, imageBytes); //Create timmer to check whether sharpenValue has been dirtied timer = new Timer(500); timer.addEventListener(TimerEvent.TIMER, onTimer, false, 0, true); timer.start(); } |
Here we did a few things:
- Snagged a reference to the shared message channels from the main thread
- Initialized a bitmapData object using the shared byteArray
- Stored a reference to the byteArray internally, so we can write to it from now on.
- Created a Timer to check for invalidation
Next, we need to respond to SHARPEN request. That’s done in the listener for the mainToBack message channel:
0 1 2 3 4 5 6 7 8 9 10 11 | protected function onMainToBack(event:Event):void { if(mainToBack.messageAvailable){ //Get the message type. var msg:* = mainToBack.receive(); //Sharpen if(msg == "SHARPEN"){ targetSharpen = mainToBack.receive(); } } } |
Note that we don’t really do anything here other than to save off the target sharpenValue. It would be wasteful to apply a sharpen every single time it’s requested. The sharpen operation can take over 500ms to complete, but the main UI thread can issue sharpen requests @ 33ms, so obviously this would create a huge backlog if we responded to each one! Eventually your worker would likely crash.
Instead, we’ll apply the SharpenFilter inside of the TIMER handler, which is running at 500ms:
0 1 2 3 4 5 6 7 8 9 10 11 12 | protected function onTimer(event:TimerEvent):void { if(targetSharpen == currentSharpen){ return; } //Don't sharpen unless the value has changed currentSharpen = targetSharpen; //Sharpen image and copy into ByteArray var data:BitmapData = ImageUtils.SharpenImage(imageData, currentSharpen); imageBytes.length = 0; data.copyPixelsToByteArray(data.rect, imageBytes); //Notify main thread that the sharpen operation is complete backToMain.send("SHARPEN_COMPLETE"); } |
That’s all there is to that! The main thread will issue a SHARPEN command with a value, the worker will get it, update the shared byteArray and send back SHARPEN_COMPLETE. Upon receiving SHARPEN_COMPLETE the main thread will update the image using the shared byteArray.
Note: If you’re wondering about the sharpen operation itself, that is handled by an excellent gskinner.com filter.
You can download the full project files here:
- ImageWorkerExample (Includes additional code for Non-Worker example)
Happy multi-threading
Thanks for the tutorial.
Workers are not working on mobiles, right?
Nope not yet, but coming soon I think
[...] AS3多线程快速入门(二):图像处理[译] 添加评论[作者:Dom Chen 分类:ActionScript, Flash ] 原文链接:http://esdot.ca/site/2012/intro-to-as3-workers-part-2-image-processing [...]
Hi, thanks for the tutorial – i can`t see any changes of the image (sharpening) in the multithreading example…
[...] 在系列教程的第二部分中,我们研究了在一个在独立线程里执行图像处理的例子。 [...]
Hi, thanks for the tutorial – i can`t see any changes of the image (sharpening) in the multithreading example too.
I debug you code ,breakpoint nevert goto SharpenWorker: onTimer to sharp function.
maybe something work or not support transfer bytearry now .
Something is missing bec’ When i run document class (ImageWorkerExample.as) it doesn’t seem like crete backWorker for SharpenWorker.as ?
its ok its my fold my player version was 11.4.400 i replaced 11.4.402
Thanks again for these tutorials. I thought I’d be clever and spawn multiple workers – turns out it brings my machine to its knees
http://flexmonkey.blogspot.co.uk/2012/09/multiple-actionscript-workers-for-image.html
simon
Hi, I tried the code and flash player 11.5.502.149 , but it doesnt seem to work.. besides shareable for bytearray, is there something else needed to do?
the messages dont seem to return from the worker..
any ideas?
thanks!!
i can`t see any changes of the image (sharpening) in the multithreading example too….is that a joke?