Asynchronous programming with Luart

By Samir Tine, published on June 2023

Concurrent programming

Standard Lua programming model uses a unique execution flow, where each line is interpreted in a sequential manner.

But what if you want to execute two functions in the same time ? The answer is simple : you can't.
The Lua virtual machine don't support multiprocessing : a Lua program is always executed in a single core/single thread.

You can use specific modules to use multicore/multithreading, but it implies multiple Lua virtual machines with inter-process communication mechanisms to exchange values between VM.

Concurrency is a lightweight programming model that mimics parallelism : the execution flow is still sequential, but it alterns execution between different functions (or coroutines).

The underlying concept is based on waiting times. Most of the times, a program spend a lot of time waiting for something, during "blocking" operations (because these operations are blocking program execution) : waiting for user input, waiting for mouse movement, waiting for server response...

Instead of just waiting, concurrency programming model permits to change execution flow to another function (or coroutine), and so on...

This programming model is very lightweight, and Lua already implements coroutines. But using Lua coroutines is not very straightforward for beginners, as it lacks any scheduling capabilities.

Since version 1.5, Luart implements a thin layer around coroutines with an integrated scheduler, enabling efficient concurrency in Lua, by using the Task object.

Let's dive into it !

The Task object

A Task is an object that wraps a Lua coroutine internaly. To create a Task, just use its constructor, with a function as argument. This function will be executed once the Task is started :

function hello() print("Hello World !") end task = sys.Task(hello)

Now run this script, but you won't see the Hello World ! message printed to the console, because when created a Task is not started, you have to start it manually. To start a Task, just call it like a function :

function hello() print("Hello World !") end task = sys.Task(hello) task()

Any argument provided when starting the Task will be used to call the function provided during the Task constructor call. Let's see it by customizing the Hello World ! message :

function hello(name) print("Hello "..name.." !") end task = sys.Task(hello) -- Starts the Task using "LuaRT" as argument for the hello() function task("LuaRT")

Your first concurrent program

For now, we have not used any concurrent paradigm yet, as we could have called the hello function directly...
Let's going a little further by printing the message after a delay, by using the sleep() function :

function hello(name) -- wait for 2sec before printing the message sleep(2000) print("Hello "..name.." !") end task = sys.Task(hello) task("LuaRT")

You may ask why the program exits without waiting and printing the message ?

The reason is simple : when called inside a Task, the sleep() function don't wait for the time to elapse, it puts the current Task to sleep and bring back execution flow to another Task or to the global execution flow. To illustrate this, we will add a last line of code :

function hello(name) -- wait for 2sec before printing the message sleep(2000) print("Hello "..name.." !") end task = sys.Task(hello) task("LuaRT") -- prints a message before exiting print("Program is exiting now !")

At the end of the program, the execution flow is stopped and the Task is cancelled. If you want to wait for a specific Task to terminate, you will need to call the Task.wait() method :

function hello(name) sleep(2000) print("Hello "..name.." !") end task = sys.Task(hello) task("LuaRT") -- Wait fot the task to terminate task:wait() print("Program is exiting now !")

Congratulation, you just have coded your first concurrent program with Luart !
As you can see, the Task.wait() function stops current execution flow until the Task is terminated, hence the message printed after 2 seconds. Now we will write several messages one after another, with a delay of one second between :

function hello(msg) sleep(1000) print("Hello "..msg.." !") n = n+1 -- Create and start only 3 Tasks, not more if n < 4 then sys.Task(hello)("from task #"..n) end end task = sys.Task(hello) -- Keeps track of current number of Tasks n = 1 task("from task #"..n) task:wait()

In this example, once a Task is about to terminate, we create and start a new Task, and so on, with a maximum of 3 Tasks. But in this case, we are only waiting on the first task, that's why you will see only the first message. One thing you can do is to wait for each created tasks :

function hello(msg) sleep(1000) print("Hello "..msg.." !") n = n+1 -- Create and start only 3 Tasks, not more if n < 4 then local newtask = sys.Task(hello) newtask("from task #"..n) newtask:wait() end end task = sys.Task(hello) -- Keeps track of current number of Tasks n = 1 task("from task #"..n) task:wait()

It works now as expected, but the code is bloated. Instead of waiting for each Task, you can use the waitall() function to wait for all Tasks to terminate :

function hello(msg) sleep(1000) print("Hello "..msg.." !") n = n+1 if n < 4 then sys.Task(hello)("from task #"..n) end end task = sys.Task(hello) -- Keeps track of current number of Tasks n = 1 task("from task #"..n) -- Wait for all Tasks to terminate waitall()

As you can see the waitall() function is very convenient to block execution flow until all Tasks are terminated.
Here is a more simple approach to the previous example :

function hello(delay, msg) sleep(delay) print("Hello "..msg.." !") end for n=1,3 do sys.Task(hello)(i*1000, "from task #"..n) end waitall()

Well this first tutorial on concurrent programming with Luart comes to its end. You have learned the basics to create and start a new Task, to bring execution flow to other Tasks, and to wait on them.


The next part of the concurrent programming tutorial will cover the famous async/await functions.