Skip to content

Lua 协程

Lua 中的协程(Coroutines)提供了一种协作式多任务(cooperative multitasking)的形式。与抢占式多任务系统(preemptive multitasking systems)中的线程(threads)不同,协程是显式地让出控制权(yield control),而不是被调度器(scheduler)中断。在任何给定的时间点,与一个 Lua 状态关联的协程中,只有一个在运行。一个协程可以暂停其执行(yield),之后可以从暂停的地方继续(resume)。

可以将协程想象成一个可以暂停执行然后继续执行的函数。当一个协程让出(yield)时,它可以将值传递回调用者(resumer)。当它被继续(resume)时,值可以传递给它,这些值将成为 coroutine.yield() 调用的结果。

协程对于以清晰和顺序的方式实现迭代器(iterators)、生成器(generators)、非阻塞 I/O 操作(non-blocking I/O operations)和状态机(state machines)非常有用。

coroutine 库提供了以下关键函数:

函数签名描述
coroutine.create(f)创建一个新的协程,并将给定函数 f 作为其主体。返回一个表示该协程的 ‘thread’ 对象。协程初始处于挂起(suspended)状态。
coroutine.resume(co [, val1, ...])启动或继续协程 co 的执行。任何参数 val1, ... 会作为参数传递给协程函数(如果是首次 resume),或者作为 yield 调用的结果(如果 resume 一个已 yield 的协程)。成功时返回 true 和传递给 coroutine.yield(或协程函数返回)的任何值。失败或协程已完成时返回 false 和错误消息。
coroutine.yield(...)暂停调用协程的执行。传递给 yield 的任何参数都将作为相应 coroutine.resume 调用的结果返回。当被 resume 时,yield 会返回传递给 coroutine.resume 的任何值。
coroutine.status(co)返回协程 co 的状态字符串:"running"(当前正在运行的协程)、"suspended"(等待被 resume)、"normal"(活跃但不是当前运行的协程,例如已 yield 给另一个协程)或 "dead"(已完成执行或因错误停止)。
coroutine.wrap(f)类似于 coroutine.create,但返回一个函数,该函数被调用时会 resume 协程。与 coroutine.resume 返回错误状态不同,这个包装函数在协程遇到错误时会抛出错误。
coroutine.running()返回当前正在运行的协程(一个 ‘thread’ 对象)以及一个布尔值,该布尔值为 true 表示当前运行的协程是主协程。
coroutine.isyieldable()(Lua 5.2+)如果当前运行的协程可以 yield,则返回 true。在 C 函数内部、pcall 或迭代器中不可 yield。

让我们看一个简单的示例来说明其流程:

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)))

运行上述程序时,输出将是(或类似):

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

示例解释:

  • 创建了一个协程 co。它最初处于挂起状态(suspended)。
  • 第一次 coroutine.resume(co, 10, 5):启动 co,将 10 和 5 作为参数传递进去。协程打印、计算 sum,然后 yield 出 sum (15) 和 “first_yield”。控制权返回到 main。
  • 第二次 coroutine.resume(co, 3, 4):继续 co。值 3 和 4 成为 co 内部 coroutine.yield 调用的结果。协程继续执行,计算 product,然后 yield 出 product (12) 和 “second_yield”。
  • 第三次 coroutine.resume(co, 100):继续 co。100 成为第二次 yield 的结果。协程完成其主体并返回两个值。
  • 协程现在处于 dead 状态。尝试再次 resume 它会导致错误。

示例:使用 coroutine.wrap 创建生成器

Section titled “示例:使用 coroutine.wrap 创建生成器”

coroutine.wrap 对于创建迭代器式函数(生成器)非常方便。

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

预期输出:

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

区分 Lua 协程和操作系统线程是很重要的。协程是协作式的:它们一次只运行一个,并且只在当前运行的协程显式地 yield 时才切换。操作系统线程(OS threads)可以并发运行(在多核系统上),并通过操作系统进行抢占式调度。协程比操作系统线程轻量得多,并且在单个操作系统进程/线程内进行管理。

应用场景:

  • **迭代器:**按需生成一系列值。
  • **任务调度:**在单个 Lua 程序中实现简单的协作式任务调度器。
  • **异步操作:**通过在等待时 yield 并在就绪时 resume 来模拟 I/O 或长时间计算的非阻塞行为(常用于 OpenResty 等 Web 服务器框架中)。
  • **游戏 AI:**将复杂的 AI 行为实现为可以暂停和继续的状态机。
  • **解析器:**编写可以 yield 中间结果或请求更多输入的解析器。

更多资源:Lua 官方参考手册提供了关于 coroutine 库及其行为的详细解释。