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:

  1. 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.
  2. 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.
  3. 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:

  1. 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.
  2. 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.
  3. 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