Lua Coroutines
Lua - Coroutines
Section titled “Lua - Coroutines”Introduction to Coroutines
Section titled “Introduction to 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.
Core Coroutine Functions
Section titled “Core Coroutine Functions”The coroutine library provides the following key functions:
| Function Signature | Description |
|---|---|
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. |
Example: Basic Yield and Resume
Section titled “Example: Basic Yield and Resume”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_valend)
print("Main: Coroutine status:", coroutine.status(co)) -- suspended
local status, yield_val1, yield_val2 = coroutine.resume(co, 10, 5) -- Pass 10 and 5 to coroutineprint(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 yieldprint(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 yieldprint(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 coroutineprint(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: suspendedCoroutine: Received initial_a = 10, initial_b = 5Coroutine: Calculated sum = 15Main: Resume 1 - Status: true, YieldVal1: 15, YieldVal2: first_yieldMain: Coroutine status: suspendedCoroutine: Resumed with resume_val1 = 3, resume_val2 = 4Main: Resume 2 - Status: true, YieldVal3: 12, YieldVal4: second_yieldMain: Coroutine status: suspendedCoroutine: Resumed with final_resume_val = 100Main: Resume 3 - Status: true, ReturnVal1: Coroutine finished, ReturnVal2: 112Main: Coroutine status: deadMain: Resume 4 - Status: false, Error: cannot resume dead coroutineExplanation of the example:
- A coroutine
cois created. It’s initially suspended. - First
coroutine.resume(co, 10, 5): Startsco, passing10and5as arguments. The coroutine prints, calculatessum, and thenyieldssum(15) and “first_yield”. Control returns tomain. - Second
coroutine.resume(co, 3, 4): Resumesco. The values3and4become the result of thecoroutine.yieldcall insideco. The coroutine continues, calculatesproduct, andyieldsproduct(12) and “second_yield”. - Third
coroutine.resume(co, 100): Resumesco.100becomes the result of the secondyield. The coroutine finishes its body and returns two values. - The coroutine is now
dead. Attempting to resume it again results in an error.
Example: Generator using coroutine.wrap
Section titled “Example: Generator using coroutine.wrap”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 previouslyExpected output:
Calling generator for the first time:Generator started.Generator yielding 1Received: 1
Calling generator for the second time:Generator yielding 2Received: 2
Calling generator for the third time:Generator yielding 3Received: 3
Calling generator after it's finished:Generator finished.Received (final call): nilCoroutines vs. Threads
Section titled “Coroutines vs. Threads”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.