Asynchronous Lua Execution
Asynchronous code execution is a common need when developing an application, and Bounces provides a set of tools for writing asynchronous Lua code. In this document, you will learn how to run asynchronous Lua code blocks using doAsync
, and how to use Lua messages and timers for synchronizing various parts of your app.
Asynchronous Lua in Bounces
Multi-threaded Lua execution
Lua code in Bounces seamlessly integrates with the target application threading architecture, and in most case you don't need to worry about multi-thread execution details when you write Lua code.
However, having an overall understanding of the execution context of your Lua code can help you to better understand some behavior of your target application, and such is precisely the goal of this section.
First thing first, we need to make a distinction between Lua threads and OS threads. The Bounces Lua runtime can execute multiple Lua threads concurrently, each Lua thread having its own callstack. A Lua thread runs in the context of an OS native thread, but since Lua does not support preemptive multitasking, only one Lua thread can be running at a given time; other active Lua threads are pending, waiting until a Lua scheduling event switches the control to the next waiting Lua thread. When you interrupt the program execution in Bounces's debugger, you can see all currently active Lua threads and examine their callstacks.
By default, Bounces runs Lua threads in application background threads, so as to avoid loading the app main thread and keep the app's main loop responsive. Background thread execution is typically what happens when a Lua module is loaded or when asynchronous execution is asked from Lua code (using doAsync
, Lua timers or message communication, see below).
On the other hand, when Lua code is directly called from native code —e.g. when an object method implemented in Lua is called—, Lua code execution takes place as expected in sequence in the caller's thread.
In addition, because a significant number of heavily-used OS SDK classes expect that their methods are called in the application main thread, Lua methods of these classes show the same main-thread-only behavior. For example, if your app defines a UIViewController
subclass and implement some or all of its methods in Lua, these methods will be called in the context of the app main thread.
Naturally, this main-thread-only behavior extends to asynchronous Lua code execution, Lua timers or message handlers of the corresponding classes.
Executing Lua code asynchronously
You use the doAsync
method to execute a block of Lua code asynchronously or after a certain delay, in a different Lua thread.
The doAsync
method
doAsync
is generally called as a method on an object related with the asynchronous code to be executed:
anObject:doAsync ([timeout], methodName_or_function, [parameters...])
where:
Parameter | Description |
---|---|
timeout | (Optional) If provided, specifies a delay before execution of the asynchronous function; if not present, the function will be executed asynchronously as soon as possible. |
methodName_or_function | This parameter can be a string or a function: if a string, it contains the name of a method of the caller object; otherwise it specifies the function to be called asynchronously. Tip: if a function is provided and its first parameter is self , the caller object is passed in this parameter when the asynchronous function is called. |
parameters | (Optional) Extra parameters passed to the asynchronous function. |
For example, when a Bounces project is configured for dynamic storyboard updates, the code below calls the StoryboardMonitor:setupExistingViewControllers
method asynchronously, i.e. after all required modules have been completely loaded:
local function monitorStoryboardViewControllers ()
local StoryboardMonitor = require 'StoryboardMonitor'
-- ...
StoryboardMonitor:doAsync ('setupExistingViewControllers')
end
monitorStoryboardViewControllers ()
require "AlbumPageViewController"
require "PhotoViewController"
In this second example, the provided function is executed after a 0.1s timeout, passed as doAsync
first parameter. The self
parameter of the provided function receives the caller object of doAsync
, here scrollView
.
scrollView:doAsync (0.1, function(self)
self.zoomScale = zoomScale
self.contentOffset = contentOffset
end)
The doAsync
function
In case asynchronous execution is not related to an object, you can use the equivalent doAsync
function of the message
Lua global table:
message.doAsync ([timeout], function, [parameters...])
Note: while method-based doAsync
is executed in the application main thread when called on a main-thread-preferring object, message.doAsync
always calls the asynchronous function in a background thread.
Using Messages for communication and notifications
The Bounces Messaging Framework broadcasts asynchronous messages from one sender to many receiver objects.
A message is a flexible entity composed of a message identifier and of optional additional parameters that can be of any type supported by Lua.
Posting a message
A message is posted by calling the function message.post(messageIdentifier, ...)
, where:
messageIdentifier
is a Lua value that identifies the message. It can be of any type, although generally a string is used as identifier.- additional optional parameters are used to provide message data. Message data can be of any Lua type.
Posting a message is an asynchronous operation: it does not block the sender and the execution of the receiver's message handler methods takes place asynchronously, possibly in a different thread.
Receiving messages
Messages reception takes place in Lua objects.
In order to receive messages with a given identifier, an object has to declare a message handler for this message identifier. It does so by calling the addMessageHandler
method:
anObject:addMessageHandler (messageIdentifier, messageHandler)
where:
messageIdentifier
: the identifier(s) of the subscribed message(s). You can subscribe to multiple messages in a single call by providing a table of identifiers like{id1, id2, id3}
.messageHandler
: a method name or a Lua function, that shall be called to handle the subscribed message(s).
Parameters of a message handler receive the same values that have been passed to the corresponding message.post
, in the same order and starting with the message identifier.
For objects that prefer the main thread, their message handlers are called in the app main thread, otherwise a background thread will be used.
An object can have at most one message handler for a given identifier. Therefore, if addMessageHandler
is called multiple time on a same object for a same message identifier, only the latest message handler is kept.
When reception of a given message is not needed anymore, the corresponding message handler can be removed by calling the removeMessageHandler
method on the receiving object:
anObject:removeMessageHandler (messageIdentifier)
where messageIdentifier
is a message identifier, a table of message identifiers or nil. If messageIdentifier
is nil, all message subscriptions are removed for the target object.
Message communication examples
A simple event with no associated data:
sender:
message.post ("This is a long message identifier")
receiver:
The receiver can pass the name of a message handler method:
function MyClass:init() -- Declare a message handler self:addMessageHandler ("This is a long message identifier", "doSomeStuff") end function MyClass:doSomeStuff(messageId) print ("Received " .. messageId) end
Alternatively the receiver can pass a handler function to
addMessageHandler
:function MyClass:init() -- Declare a message handler self:addMessageHandler ("This is a long message identifier", function(messageId) print ("Received " .. messageId) end) end
Note however that directly passing a handler function like this will cause changes in this function to be ignored in case of dynamic code update.
Passing data in a message is straightforward:
sender:
-- message data include: touchPoint, velocity, alienInfo message.post ("UserDidHitAlien", touchPoint, 24.3, { life = 5, strength = 12, intelligence = 1 })
receiver:
The message handler's parameters simply are the parameters passed to the
message.post
method.function AlienHandler:startSubscriptions() -- Declare a single message handler for two similar messages self:addMessageHandler ({"UserDidHitAlien", "AlienDidHitUser"}, "handleContactWithAlien") end function AlienHandler: handleContactWithAlien (messageId, touchPoint, velocity, alienInfo) if messageId == "UserDidHitAlien" then -- ... elseif messageId == "AlienDidHitUser" then -- ... end end
You can use any Lua value or object as message identifier, which is great to avoid message-name collisions:
sender:
function BluetoothPeripheral:NotifyChangedState(oldState) -- The peripheral object is used as message identifier message.post (self, self.state, oldstate) end
receiver:
Here a Bluetooth controller object has to monitor state changes for a set of Bluetooth peripherals.
function BluetoothController:monitorPeripheral(aPeripheral) -- The peripheral object is used as message identifier self:addMessageHandler (aPeripheral, "handlePeripheralStateChange") end function BluetoothController:stopMonitoringPeripheral(aPeripheral) self: removeMessageHandler (aPeripheral) end function BluetoothController: handlePeripheralStateChange (peripheral, newState, oldState) if newState == BluetoothPeripheral.State.connected then -- handle connected peripheral else -- other states... end end
Note the use of
removeMessageHandler
to stop receiving messages for a given peripheral.
Suspending messages reception
Sometimes it may be useful for an object to temporarily stop handling messages, and later to re-enable existing message handlers without having a precise knowledge of the received messages. This is the role of two methods: suspendMessageHandlers
and unsuspendMessageHandlers
.
For example, an instance of the AlienHandler
class above can be temporarily disabled by defining a simple class extension like this:
local AlienHandler = class.AlienHandler:addClassExtension 'activation'
function AlienHandler:deactivate ()
self:suspendMessageHandlers()
-- do other stuff...
end
function AlienHandler:activate ()
self:unsuspendMessageHandlers()
-- do other stuff...
end
Bounces predefined messages
A few messages are defined and posted by the Bounces library. Their role is to notify your program when a class, code module or a resource is (re)loaded. These messages are related with the following events:
- a Lua class has been updated,
- a Lua module has been loaded or dynamically updated.
Your code can declare message handlers for these messages, just like for user-defined messages.
Class updated message
When a Lua module declaring a class creation or extension has just been loaded, a class-updated message is sent. The message identifier for this message is the created / updated class object.
This message has the following content:
Message data | Description |
---|---|
class | the created / updated class object (message identifier ) |
module_path | dot-separated module path of the loaded Lua module, as defined in the Lua modules documentation. |
Usage notes:
- This message is used to trigger display or data-structure updates when the code of a Lua class is changed. For example, a TableViewController might want to reload its associated table view to take into account changes in its data-source or delegate methods.
- This message uses a class as message identifier. This provides easy class-based filtering for update handlers, independently of the structure of Lua modules containing extensions of this class.
- To avoid conflicts with this message, your code shall not post any message using a class as message identifier.
Lua module loaded message
When a Lua module has just been loaded, a 'system.did_load_module'
message is sent.
This message has the following content:
Message data | Description |
---|---|
'system.did_load_module' | a constant string (message identifier ) |
module_path | dot-separated module path of the loaded Lua module, as defined in the Lua modules documentation. |
module_result 1 | The first result returned by the module if any |
... | |
module_result n | The last result returned by the module if any |
Usage notes:
- This message is mainly used to trigger some updates when a Lua module is updated. For example, a ViewController might want to refresh its associated view if any module in the current program is updated.
- A module-loaded message is sent for every Lua module loaded by the target application. So if an object in your program declares a message handler for this message, this handler should generally do some kind of filtering on the Lua module path parameter.
- For a module including a Lua class creation or extension, both a class-updated message and a module-loaded message are sent. Since these messages have rather similar uses, your program should generally to avoid reacting to both messages for a given module / class.
Using Lua timers
Bounces provides a timer class for periodic execution of Lua code blocks. This class is available as class.Timer
.
You create a timer by calling Timer:new()
:
local aTimer = class.Timer:new (timeInterval, [repeats], timerFunction)
where
Parameter | Description |
---|---|
timeInterval | The time interval in seconds until the first execution of the timer and if, the timer is repeating, the time interval between timer executions. |
repeats | (Optional) If provided, specifies if the timer is repeating. Defaults to true (repeating timer). |
timerFunction | A Lua function executed each time the timer is executed. |
A timer is automatically started when created.
Example: use a timer to increment a ticks count and post a "timerTick" message every 2 seconds:
function SetupController:start ()
self.timerTicks = 0
self.timer = class.Timer:new(2.0, function()
self.timerTicks = self.timerTicks + 1
message.post("timerTick")
end)
end
Timers can be paused and restarted:
function SetupController:pause()
self.timer:pause()
end
function SetupController:resume()
self.timer:restart()
end
To terminate a timer, you simply pause it. When there is no reference left to a timer, the Lua Garbage Collector will deallocate it some time later.
function SetupController:terminate()
self.timer:pause()
self.timer = nil
end
Related documentation
Asynchronous Lua code execution and message handlers work in close relationship with Lua objects and classes. You should be familiar with the Lua object framework when using them.
Post a Comment