[转载] Asynchronous ActionScript Execution
Asynchronous ActionScript Execution
Date | September 19, 2009 |
---|---|
Language | ActionScript 3.0 |
Target | Flash Player 9+ |
Introduction
In Flash Player, both the execution of ActionScript and screen rendering is handled in a single thread. In order for the screen to be rendered, all code execution must be completed. For a Flash Player SWF running at 24 frames per second, this allows all ActionScript operations run within a single frame (frame scripts, events, etc.), at most, around 42 milliseconds to complete - this not accounting for the amount of time necessary to perform the actual screen rendering which itself may vary. If the execution of a block of code requires more time, Flash Player will appear to lock up until it is finished, or, after a default timeout period, just stop executing code.
Script timeout error dialog (Debugger Player only)
This tutorial will cover solutions to this problem - when you have operations that contain ActionScript calculations requiring more time than that which is allotted within any given frame. Such calculations are needed to be executed asynchronously; they would be non-blocking and complete at some point in time after the code block which invoked them has completed its own execution. This would give the renderer a chance to draw one or more times before the calculation is complete preventing the player from locking up.
Requirements
- A moderate understanding of ActionScript 3.0
- A compiler for ActionScript 3.0 (i.e. Flash Professional, Flash Builder, Flex SDK)
- Source files (Flash CS4 format; ActionScript externalized)
Table of Contents
- Introduction
- Requirements
- Concepts
- Single-dimension Loops
- Multi-dimension Loops
- Handling for..in and for..each Loops
Concepts
A running Flash Player instance processes a constant loop of consecutive frames. Even if there is not necessarily a timeline to animate through, frames are still rendered in the sense that a single frame representing the SWFs contents is repeatedly processed. Each frame consists of what can broken down into 2 parts: the execution of ActionScript through the AVM or ActionScript Virtual Machine, and a visual rendering of the screen by the Flash Player renderer. Code execution consists of both running ActionScript as well as idle time leading up to the next render. ActionScript is arbitrarily executed based on actions and events that occur between screen renders so idle time and occurrences may vary.
Frames both execute ActionScript and render the screen
When idle time is eaten away by the execution of cpu-intensive ActionScript, the time between each frame render will take longer, and the playback frame rate will suffer. What was 1 frame executed in 42 milliseconds may instead take 62 milliseconds, or even more if the ActionScript requires it.
ActionScript taking a long time to complete delays rendering
If ActionScript could be processed in a different thread of execution, this could prevent the renderer from being blocked. This is, however, not possible, as Flash Player, by nature, uses only one thread for both code execution and the rendering of the screen. So to prevent processor-intensive ActionScript from blocking the renderer, its calculations must be broken up into smaller, individual segments, or chunks, that can be run individually across multiple frames. Between each segment, the renderer can redraw allowing playback to go uninterrupted.
ActionScript stretched across multiple frames
This division of a single block of executing code across multiple frames is often non-trivial. Such code needs not only to know when to stop, but how to get back to where it was when picking up from where it left off. This adds the dependency of persistent data. What was before a function block with local variables must now become an object with property values.
Single-dimension Loops
The simplest, and probably the most common, circumstance requiring code to be segmented into chunks is with looping. A long loop can easily take up a lot of processing time, especially if heavy calculations are being made within each iteration of that loop.
Standard for loops use an index variable to keep track of a location within a list of items to be handled during iteration. A standard array loop would look something like:
// pseudo code var i, n = array.length; for (i=0; i<n; i++){ process(array[i]); }
Segmenting the loop so that it can be handled in multiple iterations would require breaking out of the loop in the middle of the iteration with the ability to return to it later starting with the value of the index i
at the point in time when the break occurred. The retained value of i
represents the persistent variable that needs to be saved so that it can be referred to in another frame.
// pseudo code var savedIndex = 0; var i, n = array.length; for (i=savedIndex; i<n; i++){ if (needToExit()){ savedIndex = i; break; } process(array[i]); }
Since the point of segmenting is to allow frame rendering, the execution of each chunk is appropriately handled in an Event.ENTER_FRAME event. An event handler is set when the loop starts and would need to be removed when the loop is complete. The loop is complete when the loop finishes without being exited, which, in the conext of a function would be handled by return
.
// pseudo code var savedIndex = 0; function enterFrame() { var i, n = array.length; for (i=savedIndex; i<n; i++){ if (needToExit()){ savedIndex = i; return; } process(array[i]); } complete(); }
The determination for loop exiting is decided by needToExit()
whose implementation is ultimately up to you. This can be based on a set number of iterations, or even based off of an elapsed period of time as determined by getTimer()
. Ideally, the time allotted for execution would be just enough to fill all available idle time up until the next render. There is no way to know how much time that actually is, so approximations will no doubt be a part of this process.
Example: Caesar Cipher
This example puts to practical application the looping of the characters within a string divided over multiple frames. To keep things self-contained and organized, a class, CaesarCipher, will be used to retain the necessary persistent data for the loop, though this data could also be retained within the current working scope (such as in timeline variables in Flash authoring).
The CaesarCipher class applies a Caesar cipher to a string. A standard, synchronous function for this operation would appear as:
function caesarCipher(text:String, shift:int = 3):String { var result:String = ""; var i:int; var n:int = text.length; for (i=0; i<n; i++){ var code:int = text.charCodeAt(i); // shift capital letters if (code >= 65 && code <= 90){ code = 65 + (code - 65 + shift) % 26; } // shift lowercase letters if (code >= 97 && code <= 122){ code = 97 + (code - 97 + shift) % 26; } result += String.fromCharCode(code); } return result; }
The CaesarCipher class uses this same logic but allows the operation to continue over multiple frames before it's considered complete. This process then becomes asynchronous. Certain things had to be considered when implementing this:
- Only display objects receive Event.ENTER_FRAME events
- Return values no longer apply to asynchronous operations
The goal of the CaesarCipher class is to processes data; instances of it are not meant to be displayed on the screen. As a result, it will not be based off of another DisplayObject class which keeps the Event.ENTER_FRAME event from being available. This dependency, however, can be easily satisfied with composition, including a simple DisplayObject class member such as a Shape instance within the CaesarCipher class definition. Even though that instance will never reach a display list, it will still dispatch the Event.ENTER_FRAME event that can be used by an instance of CaesarCipher.
Concerning return values, an approach used by existing implementations of asynchronous operations will need to be used. For example, consider the URLLoader class. Instances are used to load external content through an asynchronous operation started by the load()
call. When complete, an Event.COMPLETE event is dispatched and the loaded data can be accessible through the data
property of the URLLoader instance. The CaesarCipher class uses a similar implementation. To start the operation, a run()
method is used. When complete, an Event.COMPLETE event will fire, and the would-be return value can be accessible through the result
property.
- The CaesarCipher class (available in source files)
Usage:
var text:String = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
var cipher:CaesarCipher = new CaesarCipher(text, 3);
cipher.addEventListener(Event.COMPLETE, complete);
cipher.run();
function complete(event:Event):void {
trace("Result: " + cipher.result);
// Result: Oruhp lsvxp groru vlw dphw, frqvhfwhwxu dglslvflqj holw.
}
View live CaesarCipher example.
Though this class retains its result data in a class member variable, it could also be passed passed to the event, for example, if using a TextEvent object for the complete event. This approach, however, would necessitate the creating of a new Event class to handle the results data, so it's usually easier to stick to a class variable.
Because this example is using a short string, and because a Caesar cipher is not especially difficult for ActionScript to pull off, an iteration counter was used to determine when the next frame should be allowed to render between processing steps. It may be more common that a time-based approach will be used to make sure you're getting the most out of what time you have to calculate results in your frame.
Time-based Exit Conditions
Exiting an asynchronous script based off of the time taken to process a calculation will likely be the best solution for handling the segmentation. For this, you would need to know how much time has passed since the current frame's calculations started, and how much time, per frame, the calculations are allowed to have.
//pseudo code var allowedTime = 1000/fps - renderTime - otherScriptsTime; var startTime = 0; var savedIndex = 0; function enterFrame() { startTime = getTimer(); var i, n = array.length; for (i=savedIndex; i<n; i++){ if (getTimer() - startTime > allowedTime){ savedIndex = i; return; } process(array[i]); } complete(); }
The times for rendering and other scripts will be estimated, but the more accurate they are, the better use of the available frame time you'll get. Note that these values can change depending on processor speed of computer running the script which can make them variable even at runtime. The closer it is to filling up the time available in the frame the better. You may even want to overshoot a little bit to make processing go as fast as possible at the expense of the frame rate depending on what's being presented to the user when the operation is being run.
For operations that contain a lot of looping with smaller calculations, you may not want the exit condition to be checked in every loop iteration - this especially when using getTimer()
since it will help reduce the overhead of calling the function and looking up that value. The fewer checks made, the faster processing will go. It can be a tricky balancing act between when to do what and how many times to do it.
Most examples covered here will not use time for exit conditions simply for means of demonstration, though they may be better suited to use it, especially for larger scale implementations.
Multi-dimension Loops
Data represented in multiple dimensions, such as grid data, or the pixels in an image, often necessitate nested loops. When converting these loops into asynchronous operations, a similar approach to the asynchronous single loops is used. With nested loops, however, extra attention is needed to facilitate the initialization of loop variables when resuming a the loop. Specifically, it becomes important that saved indices for nested loops be reset to 0 at the end of the loop for its next iteration.
// pseudo code for (i=savedIndexI; i<n; j++){ for (j=savedIndexJ; j<m; j++){ process(array[i][j]); } savedIndexJ = 0; }
The resetting of the initial index value will have to be done for each nested loop and only after the first loop has already run. This is necessary to make sure the saved index is not reset before its able to be used for the first iteration of that loop.
Example: Color Gradient
Processing pixels in a bitmap is a common case where a nested loop would be needed. This example, using a class named RenderGradient, draws a gradient into a bitmap pixel by pixel using the setPixel()
method of the BitmapData class.
The behavior of the RenderGradient class very closely follows that of the CaesarCipher class in the previous example. The class structure is almost identical with only a few notable exceptions:
- Two loops are used; the inner loop index variable (
savedX
) is reset at the end of the loop block - A separate counter variable,
iterationCounter
, is used to count loop iterations for segmentation - Validation of the referenced BitmapData is required each time the operation is resumed
- No return variable is necessary - the operation edits existing content rather than generating its own
The validation step is one worth noting. This was not an issue with the Caesar cipher example because all utilized data was copied into class member variables of the CaesarCipher instance. For RenderGradient, a BitmapData object is stored as a reference (through the bmp
variable); it is not copied. The original BitmapData is modified as calculations are being made. This is important because if at some point the referenced BitmapData instance is disposed of by another process (using dispose()
), the calculations made by the RenderGradient class would fail. This not unlike concurrency problems encountered in multi-threaded environments. In fact, these asynchronous operations are very thread-like and with references to external objects can experience similar consequences of thread interference. With ActionScript, however, you are at least guaranteed that no two scripts are being executed at the same time. This allows a one-time validation step at the start of an operation to handle such inconsistencies or unexpected states in referenced data. RenderGradient does this in a try..catch when defining the xn
and yn
variables from the width and height of the referenced BitmapData in the loop handler. Disposed BitmapData objects will cause an error when getting the width and height values allowing RenderGradient to exit gracefully with an Event.CANCEL event should an error occur there.
RenderGradient's validation does not prevent multiple instances from acting upon the same bitmap. It would be quite possible to have multiple RenderGradient instances editing the pixels of the same bitmap. The last instance called would simply overwrite all of the changes made by the previous. Other situations may require different levels of validation.
- The RenderGradient class (available in source files)
Usage:
var renderer:RenderGradient = new RenderGradient(bmp, 0xFF0000, 0x0000FF); renderer.run();
View live RenderGradient example.
Since all changes occur during the operation and no result value is needed, no complete event handler has been included here, though it may be necessary if there are other dependencies waiting for its completion.
Handling for..in and for..each Loops
Each of the previous examples use indexed loops for pausing and resuming iteration across multiple frames. They do not consider non-indexed looping such as for..in and for..each loops. These loops lack the ability to restart at an arbitrary location within the iteration. This prevents them from working very well with segmentation used to divide loops into smaller parts.
To handle these loops, you would need to first convert them into an indexed list, then use that list with segmentation.
// pseudo code
var indexedList = [];
for each(value in object){
indexedList.push(value);
}
// proceed normally...
This does incur a little extra overhead at the beginning of the operation but assuming the for/for..each loop itself isn't the cause of the slow processing, it should be negligible.
Sequences
Sometimes you aren't dealing with loops at all, and simply have chunks of code that take a long time to execute, which when combined, may stall screen rendering. For these cases, each chunk can be separated into individual functions and run individually. This can be done through an array or a linked list.
Array Sequencing
With array sequencing, each function is stored within an array and then each is called one frame at a time. The function sequence becomes the loop, and the processing of the array element is calling the function contained within that element.
// pseudo code
function processA(){
// time consuming process
}
function processB(){
// time consuming process
}
function processC(){
// time consuming process
}
var sequence:Array = [processA, processB, processC];
// Event.ENTER_FRAME loop through sequence array ...
The sequence array can be dynamically generated at runtime allowing for a mix and match of sequential function calls. In the end, this ends up being nothing more than a problem solved by the Single-dimension Loops solution.
Linked List Sequencing
Sequencing can also be approached through a linked list rather than an array. As a linked list, each function call indicates, when it is called, the next function to be called in an operation. An Event.ENTER_FRAME loop can then continuously call any referenced function until that reference is null.
// pseudo code
function processA(){
// time consuming process
next = processB;
}
function processB(){
// time consuming process
next = processC;
}
function processC(){
// time consuming process
next = null;
}
var next = processA;
// Event.ENTER_FRAME call next while non-null ...
The advantage of linked list sequences is that called functions decide through their own logic, while being called, which function is next to be called in the sequence.
Generally sequences such as these do not have as much control over the time it takes to perform each iteration. The separation of calculations across functions is a manual process which can be hard to distribute evenly, or even develop. If divided in smaller calculations, the Event.ENTER_FRAME event handler can attempt to call multiple functions within a sequence making its own determination as to when it should wait for the next frame to continue.
Example: Custom Bitmap Filters
This example revisits bitmaps with do-it-yourself, custom filters that applied to bitmap data in pixel by pixel looping. Each filter is a single function with loops go through a bitmap's pixels synchronously. Each filter being applied is added to a sequence which is run through asynchronously, applying one filter at a time, each frame. This is all being run by the ApplyFilters class, which accepts a BitmapData instance, and a sequence of filter functions with a BitmapData input that are to be applied to the BitmapData supplied.
- The ApplyFilters class (available in source files)
Usage:
var sequence:Array = [twirl, wave, bubble]; // custom functions
var applicator:ApplyFilters = new ApplyFilters();
applicator.apply(bmp, sequence);
View live ApplyFilters example.
You may notice a slight change in design with the ApplyFilters class. Namely, the method that would have been called run()
is now named apply()
. The apply()
method is also where data is passed to the ApplyFilters class. Previous examples were provided data in their constructors. Either place is acceptable; the choice is yours to make. The approach taken here moves further away from the idea that the class instances represent function calls and treats them as tools for performing a task; it favors reuse since a single instance can be used to perform multiple operations with different data.
This particular sequence implementation assumes each filter function will take around a frame to complete, or at least assumes that running more than one a frame would be too much, though more than one could be called each frame. In doing so, the standard loop approach with a saved index would be used.
In general, sequencing tends to be more versatile. This example has a dependency on a BitmapData instance that each function in the sequence needs to be provided. But you can imagine for situations where no arguments are used, a single sequencing class can be used in multiple situations. More adept sequencing classes can even listen for completion events of other sequences before continuing to the next function call.
Recursion
Recursion - when a function calls itself - represents yet another kind of looping. Recursive operations are neither indexed nor easily converted to something that is (such was the case with for..in and for..each loops). This requires a solution separate to those used before.
// pseudo code function recursive(){ recursive(); } recursive();
When functions call one another (including themselves), they are added to the call stack. The call stack contains all of the functions currently being called. When a function completes its execution or returns a value, it is removed from the call stack and the function which called it resumes executing. Recursive functions add themselves to the call stack repeatedly until some condition is reached that the last function returns a value rather than calling itself again.
Recursive function call's call stack
When creating an asynchronous version of a recursive operation, it may be necessary from within any function call on the stack - which are all the same function - that the operation would need to be exited in favor of resuming again on the next frame. When resumed, the stack would have to be rebuilt but without performing all of the same calculations that was previously made when the stack was constructed last time. To do this, conditions can be used to make sure blocks of logic are only run once.
// pseudo code
var processed = false;
function recursive(){
if (needToExit()){
throw error;
}
if (!processed){
// process
processed = true;
}
recursive();
}
In the above sample, the first thing you might notice is that the needToExit()
condition throws an error rather than returning. Since this call could be within any number of nested function calls, throwing an error is the easiest way to escape the entire call stack. Code running the Event.ENTER_FRAME event handler would handle this with a try..catch and wait until the next frame to continue.
More importantly, this sample shows that the code within the processed block will only be run once, assuming it is ever completely run at all. If any of the nested function calls throws an error, the original function would be re-called in the next frame. As the stack rebuilds, all calculations that have already been processed will be skipped until a block that hasn't yet been run is reached and the processing continues.
Rebuilding call stack of a recursive function
Because data needs to be retained, not only in retaining the results of the calculations that were made within the process logic block but also with the necessity of the processed
flag, each function call (as has been the case thus far) will need to be encapsulated in an object. The operation is complete when the first, root object can be called and completed successfully without it or any of its nested calls having thrown an error.
Two classes are needed to make this possible, a "runner" to make the call to the root recursion object, and one to define each of the recursion objects. The runner is the public facing class handling the Event.ENTER_FRAME event and dispatching an Event.COMPLETE event when the operation is complete. The recursion class handles the processing and recursive calling. The runner would work something like:
// pseudo code var rootRecursive = new Recursive(); function enterFrame() { try { rootRecursive.call(); complete(); }catch(error){} }
Any Recursive instance can throw an error to interrupt the operation causing a delay until the next frame. If no error is caught, the recursion calculations have completed. Note that it may be important to pay careful attention to what errors are thrown or could be thrown in case they are not related to making the operation asynchronous and instead indicate some other error.
Example: Factorial
The classic recursion example is the factorial function:
function factorial(n:int):int { if(n == 0) return 1; return n * factorial(n - 1); }
This is a simple recursive function which returns the factorial of the supplied value n
. To make this function an asynchronous operation, two not so simple classes are needed - one to run (Event.ENTER_FRAME looping), and the other to handle the recursive logic. The Factorial class is represents the runner, and it's helper class FactorialOp contains the recursive logic.
- The Factorial class (available in source files)
Usage:
var factor:Factorial = new Factorial(8);
factor.addEventListener(Event.COMPLETE, complete);
factor.run();
function complete(event:Event):void {
trace("Result: "+factor.result);
// Result: 40320
}
View live Factorial example.
Much of the Factorial class is still cookie-cutter in terms of what's been covered before. Theres a run()
method, an Event.ENTER_FRAME loop and we even see the return of the result
property to contain the return value of the operation. It's the loop handler function is where we see the changes.
Instead of running a loop and retaining an index, an instance of FunctionOp is being "called" every frame (via its call()
method). When it completes without throwing an error, the operation is complete and the Event.COMPLETE event is dispatched. Each time that FunctionOp is called, it goes further and further into its recursion, rebuilding the call stack skipping any calculations that have been completed in a previous call.
There's actually no specific processed
variable to indicate what has been run for this example. Instead the existence of the instance of the next recursive FunctionOp object, recursiveOp
, is used. If that exists, it indicates that the code block in which it was created has been run and can be skipped in the next iteration.
The it is defined now, each FunctionOp will throw an error the first time its called using an interrupted
flag. This actually creates a worst case scenario for overall processesing time since at no point in time will a frame execute more than one logic block. This approach is simply being used for demonstration purposes. For practical use, it may be necessary to pass some additional information into the call()
function to know the state of the time taken or number of calculations made for a more accurately timed exit condition.
Example: Quicksort
The Quicksort sorting algorithm is another example of a recursive operation. Unlike with a factorial, quicksort uses two recusive calls. When getting into multiple recursive calls, more conditional blocking of logic is needed and it becomes more conveneint to use a single variable for identifying the execution of those blocks. The Quicksort and QuicksortOp classes do exactly that.
Instead of using a reference to the next QuicksortOp to know if logic has been executed, this Quicksort example uses a processed
variable. It's used as a counter to indicate which of the multiple blocks of logic in each QuicksortOp call has been executed.
- The Quicksort class (available in source files)
Usage:
var array:Array = [2,4,7,9,1,9,8,5,3,0,3,5,6,7,8,8,1,2,0,3,7,5,5,9];
var sorter:Quicksort = new Quicksort(array);
sorter.addEventListener(Event.COMPLETE, complete);
sorter.run();
function complete(event:Event):void {
trace("Result: "+array);
// Result: 0,0,1,1,2,2,3,3,3,4,5,5,5,5,6,7,7,7,8,8,8,9,9,9
}
View live Quicksort example.
Synchronous with Asynchronous
Sometimes you need to create an asynchronous operation that isn't limited by processing speed, but other asynchronous operations. If one operation depends on the result of another asynchronous operation, that operation then has to be asynchronous. Any kind of network operation, for example, such as loading data from a remote server, would necessitate this.
When dealing with other asynchronous actions you're dealing with events. Events indicate an asynchronous operation completes (or fails) - something examples so far have been using. When one asynchronous operation depends on others, it needs to manage the events of the dependent operations and continue with its own actions after they complete - all of which is really just standard practice in your run of the mill ActionScript development.
Where it starts to get tricky is when you have a series of operations that may or may not be synchronous. Since such operations could be possibly asynchronous, an event would be necessary to identify completion regardless of whether the operation was truely asynchronous or not. When multiple operations are being run in a series, in the completion event handler of one opration, another would start. When that operation completes, the same handler would be used to repeat the process working through each operation in that series. If all of these operations end up being synchronous, you end up creating a recursion loop within this handler. With a enough operations in a series, this could create a stack overflow, with two many functions on the call stack. Truely asynchronous operations wouldn't suffer from this because their completion event that would have originated from within a call stack separate from the one being used by the series loop.
The solution to this problem, simply, is to avoid the recursion. This can mean waiting a frame (or using some other event) to restart the next set of operations. Though with enough operations in a series, this could be time consuming compared to a single-frame solution for a long set of synchronous variations.
An alternate approach would be to use looping in place of the recursion. A simple flag in a completion event handler can be used to identify an asynchronous result which can be used in a main series loop to know whether or not the loop can continue synchrnously. If asynchronous, the same flag can be used to let the event handler know that it needs to restart the series in its new call stack.
// pseudo code var savedIndex = 0; var resume = false; function loop(){ var i, n = array.length; for (i=savedIndex; i<n; i++){ resume = false; process(array[i]); if (!resume){ resume = true; savedIndex = i + 1; return; } } } function processComplete(){ if (resume){ loop(); }else{ resume = true; } }
Here, the resume
variable indicates to the complete handler function whether or not it needs to restart the loop()
function or to do nothing allowing the existing loop()
loop already in the stack to continue synchronously. The loop, in finding that the value of resume
hasn't changed can know that the complete event hasn't occured indicating that it needs to exit and wait for that to happen.
The handling of resume
could have also been managed by a return value from process()
(which translates to run()
) indicating whether or not the operation was synchronous. But this would mean that the operation (the run()
) was designed with that behavior in mind. All previous examples covered so far, for example, are not doing this but could potentially be synchronous if they never delay calculations to another frame.
Example: Reading Inline and Remote Text
Consider an XML file that references a collection of texts that need to be combined into a single document. The XML file may have text definined inline within its own XML, or link to an external text file which would need to be loaded into the player to be accessed. Each inline text can be read synchronously while external references need to be read asynchronously (loaded then read).
One class, GetElementText, is used to retrieve the text of any single XML element that may specify inline text or reference external text. It contains all of the logic to determine which is being used and how to get it. When the text is read, it dispatches an Event.COMPLETE event.
Another class, CreateDocument, is responsible for generating the document. It runs through an XML file creating GetElementText objects to get the text for each text element and adds the results to a single string representing the completed document. When this is complete, it too will dispatch an Event.COMPLETE event. In order to manage both circumstances of the GetElementText complete event (synchronous and asynchronous), and to help avoid a possible stack overflow from recursion, a resume
variable is used to keep track of the GetElementText results as the XML is iterated over in a loop.
- The GetElementText class (available in source files)
- The CreateDocument class (available in source files)
Usage:
var document:CreateDocument = new CreateDocument(); document.addEventListener(Event.COMPLETE, documentComplete); document.create(xml); function documentComplete(event:Event):void { trace("Result:\n" + document.text); }
View live CreateDocument example.
Of course, even if every XML node were inline, and precautions weren't taken to prevent a stack overflow, one wouldn't have occurred in this example. However, with larger applications, and when working with a lot more data, that may not always be the case. It's better to be able to handle these kinds of situations in order to make sure your applications are scalable.
Conclusion
It may be uncommon that you need to create an asynchronous version of an operation, but it can be an important skill in helping you maintain fluid animations and uninterruped interactivity in your application. And that can go a long way in how users perceive your application.
You may find yourself in a situation where the topics covered here won't help. But there's almost always an alternative, whether its handing off expensive operations to a server, or using paging to limit the amount of data that you're working with at any given time. It's just a matter of finding the right solution for the job.