← Back to the index page

Understanding Coroutines in Lua

Basics

Coroutines in Lua are special threads with their own scope, stack, and instruction pointers. Coroutines are able to run simultaneously in the main thread. Another important statement that coroutines share global variables. All coroutines are stored in Lua’s built-in coroutine table.

Coroutines are not really parallel; Lua is a strictly single-threaded language. Coroutine executes the function for a while, after returns back to the point (with the same state), where it was called. Using coroutines, you can modify (and in some cases facilitate) how the code behaves, to make it more evident or legible.

Coroutine has a special type thread.

Coroutine might have 4 states:

When a coroutine is created, it starts in a suspended state.

local co = coroutine.create(function()
    print("Hello, world")
end)

print(co) --> thread: 0x69fcea8
coroutine.status(co) --> suspended

Function coroutine.resume() continues or begins to execute the coroutine.

coroutine.resume(co) --> true   "Hello, world"

After the run of the coroutine, it finishes its own lifecycle and get dead status.

coroutine.status(co) --> dead

In the example above, it makes no sense to create coroutines, because it looks like a common function call. But the most powerful feature of coroutines is corputine.yield() function, which allows you to suspend an execution of the coroutine for later. On the creation, the coroutine has suspended state. When the coroutine is finished, the statue becomes dead and the call-off with corputine.resume() returns false and the error message cannot resume dead coroutine.

Consider:

local co = coroutine.create(function()
    for i = 1, 5 do
        print("iteration " .. i)
        coroutine.yield()
    end
end)

print(coroutine.status(co)) --> "suspended"
coroutine.resume(co) --> "iteration 1"
print(coroutine.status(co)) --> "suspended"
coroutine.resume(co) --> "iteration 2"
print(coroutine.status(co)) --> "suspended"
coroutine.resume(co) --> "iteration 3"
print(coroutine.status(co)) --> "suspended"
coroutine.resume(co) --> "iteration 4"
print(coroutine.status(co)) --> "suspended"
coroutine.resume(co) --> "iteration 5"
print(coroutine.status(co)) --> "suspended"
coroutine.resume(co) --> "iteration 5"
print(coroutine.status(co)) --> "dead"

local ok, err = coroutine.resume(co)
print(ok, err) --> false    "cannot resume dead coroutine"

Illustration of coroutine lifecycle of the example above.

Illustration of coroutine lifecycle

Coroutines working with main thread

Error handling in coroutines

Tip

It is always a good practice to check for the errors.

Execution of the coroutine happens in protected mode; this means if there is an error inside coroutine body nothing will be reported.

Consider:

local co = coroutine.create(function()
    error("Kabooom!")
end)
coroutine.resume(co)
-- nothing printed nor reported

Correct way to handle the errors in the coroutine.

local co = coroutine.create(function()
    error("Kabooom!")
end)
local ok, err = coroutine.resume(co)
print(ok, err) --> false   co.lua:2: Kabooom!

Wrapping coroutines

As we already know, coroutines are implicitly running in protected mode. If there is a need to handle error with pcall or, xpcall coroutine can be wrapped with coroutine.wrap().

coroutine.wrap creates a new coroutine with the function func as its body; it funcmust be a valid function. The coroutine returns a function that resumes execution each time it is invoked. Any arguments passed to this returned function are treated as additional arguments for the resume operation. The function yields the same results as, coroutine.resume excluding the initial boolean value. If an error occurs, the coroutine is closed, and the error is propagated and can be caught with pcall or xpcall.

Consider:

local co = coroutine.wrap(function()
    error("Kabooom!")
end)
local ok, err = pcall(co)
print(ok, err) --> false   co.lua:2: Kabooom!

Arguments in the coroutines and data exchange

Coroutines can receive any number of arguments like common functions.

local co = coroutine.create(function(a, b) 
    print(a * b)
end)

coroutine.resume(co, 6, 7) --> 42

Arguments also can be yielded coroutine.yield() returns status true in case of success, or false in case of an error inside a coroutine, after the status arguments follow the coroutine.resume() function.

local co = coroutine.create(function(a, b)
    coroutine.yield("The meaning of life is", a * b)
end)

local ok, message, value = coroutine.resume(co, 6, 7)
print(message, value, ok) --> "The meaning of life is"  42  true

Basic example

Here is an artificial example, but it very well demonstrates how multiple coroutines are running simultaneously. Every coroutine has a delay number.

local heavyFunction = function(secondsDelay, number)
    local start = os.time()
    while (os.time() - start) < secondsDelay do
        -- do nothing, loop just for a delay
    end
    print(
            string.format(
                    "coroutine %d finshed after %d second(s)",
                    number,
                    secondsDelay
            )
    )
end

local co1 = coroutine.create(heavyFunction)
local co2 = coroutine.create(heavyFunction)
local co3 = coroutine.create(heavyFunction)
coroutine.resume(co1, 1, 3)
print("returned to main thread")
coroutine.resume(co2, 2, 2)
print("returned to main thread")
coroutine.resume(co3, 3, 1)
print("returned to main thread")
-- Output:
-- coroutine 3 finished after 1 second(s)
-- returned to main thread
-- coroutine 2 finished after 2 second(s)
-- returned to main thread
-- coroutine 1 finished after 3 second(s)
-- returned to main thread

HTTP request example

Making the HTTP request with coroutine, for the example below LuaSocket library is required to be installed.

local http = require("socket.http")
local URL = "https://www.whoop.ee"

local request = coroutine.create(function(url, headers)
    headers = headers or {}
    local body, status = http.request(url)
    coroutine.yield(status, #body)
end)

local ok, code, length = coroutine.resume(request, URL)
print(ok, code, length) --> true    200 7641

Advanced

Producer, filter, consumer pattern.

Pretty common pattern using coroutines: producer, filter, consumer. In this pattern there might be any number of filters between producer and consumer. Benefits of this approach are easy to maintain.

Producer, filter, consumer pattern with coroutines in Lua

---Some delay
---@oaran seconds number
local function sleep(seconds)
    -- Fox Unix-like
    os.execute(string.format("sleep %d", seconds))
    -- For Windows
    -- os.execute(string.format("timeout %d", seconds))
end

---Pong producer
---@return thread
local pong = function()
    return coroutine.create(function()
        coroutine.yield("pong")
    end)
end

---Ping producer
---@return thread
local ping = function()
    return coroutine.create(function()
        coroutine.yield("ping")
    end)
end

---Filter adds "ping|ping hit X"
---@param coPing function
---@param coPong function
local hitsFilter = function(coPing, coPong)
    return coroutine.create(function()
        local hit = 0
        while true do
            hit = hit + 1
            local _, x = coroutine.resume(coPing())
            coroutine.yield(x .. " hit " .. hit)
            hit = hit + 1
            sleep(1)
            _, x = coroutine.resume(coPong())
            coroutine.yield(x .. " hit " .. hit)
            sleep(1)
        end
    end)
end

---Play consumer
---@param filter thread
local function play(filter)
    while true do
        local _, s = coroutine.resume(filter)
        print(s)
    end
end

---Run
play(hitsFilter(ping, pong))
-- Output every second:
-- ping hit 1
-- pong hit 2
-- ping hit 3
-- pong hit 4
-- etc..

References

Feedback

For feedback, please check the contacts section. Before writing, please specify where you came from and who you are. Sometimes spammers go insane. Thank you in advance for your understanding.

← Back to the index page