Skip to content

Lua Coroutines

Coroutines in Lua provide a form of cooperative multitasking. Unlike threads in preemptive multitasking systems, coroutines yield control explicitly rather than being interrupted by a scheduler. At any given time, only one coroutine associated with a Lua state is running. A coroutine can suspend its execution (yield) and later be resumed from where it left off.

Think of a coroutine like a function that can pause its execution and then be resumed. When a coroutine yields, it can pass values back to the resumer. When it’s resumed, values can be passed into it, becoming the result of its coroutine.yield() call.

Coroutines are useful for implementing iterators, generators, non-blocking I/O operations, and state machines in a clean and sequential style.

The coroutine library provides the following key functions:

Function SignatureDescription
coroutine.create(f)Creates a new coroutine with the given function f as its body. Returns a ‘thread’ object representing the coroutine. The coroutine starts in a suspended state.
coroutine.resume(co [, val1, ...])Starts or continues the execution of coroutine co. Any arguments val1, ... are passed as arguments to the coroutine function (if it’s the first resume) or as results from the yield call (if resuming a yielded coroutine). Returns true and any values passed to coroutine.yield (or returned by the coroutine function) on success. Returns false and an error message on failure or if the coroutine has finished.
coroutine.yield(...)Suspends the execution of the calling coroutine. Any arguments passed to yield are returned as results from the corresponding coroutine.resume call. When resumed, yield returns any values passed to coroutine.resume.
coroutine.status(co)Returns the status of coroutine co as a string: "running" (the current coroutine), "suspended" (waiting to be resumed), "normal" (active but not the current running one, e.g., yielded to another coroutine), or "dead" (finished execution or stopped by an error).
coroutine.wrap(f)Similar to coroutine.create, but returns a function that, when called, resumes the coroutine. This wrapper function raises an error if the coroutine encounters an error, unlike coroutine.resume which returns an error status.
coroutine.running()Returns the currently running coroutine (a ‘thread’ object) and a boolean that is true if the running coroutine is the main one.
coroutine.isyieldable()(Lua 5.2+) Returns true if the running coroutine can yield. Not yieldable inside C functions, pcall, or iterators.

Let’s look at a simple example to illustrate the flow:

local co = coroutine.create(function(initial_a, initial_b)
print(string.format("Coroutine: Received initial_a = %s, initial_b = %s", tostring(initial_a), tostring(initial_b)))
local sum = initial_a + initial_b
print("Coroutine: Calculated sum =", sum)
local resume_val1, resume_val2 = coroutine.yield(sum, "first_yield") -- Yield sum and a string
print(string.format("Coroutine: Resumed with resume_val1 = %s, resume_val2 = %s", tostring(resume_val1), tostring(resume_val2)))
local product = resume_val1 * resume_val2
local final_resume_val = coroutine.yield(product, "second_yield") -- Yield product and another string
print("Coroutine: Resumed with final_resume_val =", final_resume_val)
return "Coroutine finished", product + final_resume_val
end)
print("Main: Coroutine status:", coroutine.status(co)) -- suspended
local status, yield_val1, yield_val2 = coroutine.resume(co, 10, 5) -- Pass 10 and 5 to coroutine
print(string.format("Main: Resume 1 - Status: %s, YieldVal1: %s, YieldVal2: %s", tostring(status), tostring(yield_val1), tostring(yield_val2)))
print("Main: Coroutine status:", coroutine.status(co)) -- suspended
local status2, yield_val3, yield_val4 = coroutine.resume(co, 3, 4) -- Pass 3 and 4 to the waiting yield
print(string.format("Main: Resume 2 - Status: %s, YieldVal3: %s, YieldVal4: %s", tostring(status2), tostring(yield_val3), tostring(yield_val4)))
print("Main: Coroutine status:", coroutine.status(co)) -- suspended
local status3, return_val1, return_val2 = coroutine.resume(co, 100) -- Pass 100 to the last waiting yield
print(string.format("Main: Resume 3 - Status: %s, ReturnVal1: %s, ReturnVal2: %s", tostring(status3), tostring(return_val1), tostring(return_val2)))
print("Main: Coroutine status:", coroutine.status(co)) -- dead
local status4, err_msg = coroutine.resume(co) -- Attempt to resume a dead coroutine
print(string.format("Main: Resume 4 - Status: %s, Error: %s", tostring(status4), tostring(err_msg)))

When the above program is run, the output will be (or similar):

Main: Coroutine status: suspended
Coroutine: Received initial_a = 10, initial_b = 5
Coroutine: Calculated sum = 15
Main: Resume 1 - Status: true, YieldVal1: 15, YieldVal2: first_yield
Main: Coroutine status: suspended
Coroutine: Resumed with resume_val1 = 3, resume_val2 = 4
Main: Resume 2 - Status: true, YieldVal3: 12, YieldVal4: second_yield
Main: Coroutine status: suspended
Coroutine: Resumed with final_resume_val = 100
Main: Resume 3 - Status: true, ReturnVal1: Coroutine finished, ReturnVal2: 112
Main: Coroutine status: dead
Main: Resume 4 - Status: false, Error: cannot resume dead coroutine

Explanation of the example:

  • A coroutine co is created. It’s initially suspended.
  • First coroutine.resume(co, 10, 5): Starts co, passing 10 and 5 as arguments. The coroutine prints, calculates sum, and then yields sum (15) and “first_yield”. Control returns to main.
  • Second coroutine.resume(co, 3, 4): Resumes co. The values 3 and 4 become the result of the coroutine.yield call inside co. The coroutine continues, calculates product, and yields product (12) and “second_yield”.
  • Third coroutine.resume(co, 100): Resumes co. 100 becomes the result of the second yield. The coroutine finishes its body and returns two values.
  • The coroutine is now dead. Attempting to resume it again results in an error.

coroutine.wrap is convenient for creating iterator-like functions (generators).

local function number_generator(limit)
return coroutine.wrap(function()
print("Generator started.")
for i = 1, limit do
print(string.format("Generator yielding %d", i))
coroutine.yield(i)
end
print("Generator finished.")
-- No explicit return means it implicitly returns nothing when done.
end)
end
local next_num = number_generator(3)
print("Calling generator for the first time:")
print("Received:", next_num()) -- Calls resume implicitly
print("\nCalling generator for the second time:")
print("Received:", next_num())
print("\nCalling generator for the third time:")
print("Received:", next_num())
print("\nCalling generator after it's finished:")
-- Depending on Lua version and context, this might error or return nil.
-- Typically, if wrapped function has no return, calling it after exhaustion returns nil.
-- If it had a return, that would be returned. If it errors, wrap raises the error.
local final_val = next_num()
print("Received (final call):", final_val) -- Expected: nil after exhaustion
-- print(next_num()) -- Calling again might error if it errored previously

Expected output:

Calling generator for the first time:
Generator started.
Generator yielding 1
Received: 1
Calling generator for the second time:
Generator yielding 2
Received: 2
Calling generator for the third time:
Generator yielding 3
Received: 3
Calling generator after it's finished:
Generator finished.
Received (final call): nil

It’s important to distinguish Lua coroutines from operating system threads. Coroutines are collaborative: they run one at a time and only switch when the running coroutine explicitly yields. OS threads can run concurrently (on multi-core systems) and are preemptively scheduled by the OS. Coroutines are much lighter weight than OS threads and are managed within a single OS process/thread.

Application Scenarios:

  • Iterators: Generating sequences of values on demand.
  • Task Scheduling: Implementing simple cooperative schedulers for tasks within a single Lua program.
  • Asynchronous Operations: Simulating non-blocking behavior for I/O or long computations by yielding while waiting and resuming when ready (often used in frameworks like OpenResty for web servers).
  • Game AI: Implementing complex AI behaviors as state machines that can pause and resume.
  • Parsers: Writing parsers that can yield intermediate results or request more input.

Further Resources: The official Lua Reference Manual provides a detailed explanation of the coroutine library and its behavior.