什么是 Lua 协同程序(coroutine)?
在编写程序时,我们常常遇到需要“暂停”某个任务,稍后再继续执行的场景。比如:下载一个大文件时,不想阻塞整个程序;或者处理多个任务,希望它们能轮流运行,而不是一个接一个地执行。这时候,传统的“函数调用”方式就显得力不从心了。
Lua 提供了一种强大的机制来解决这类问题——协同程序(coroutine)。它不是多线程,也不是异步回调,而是一种轻量级的并发模型。你可以把它想象成“可以暂停和恢复的小任务”,就像一个在执行中被打断的演员,可以随时从断点继续演出,而不需要从头开始。
协同程序的核心特点是:由程序员显式控制何时暂停、何时恢复。这与操作系统调度的线程完全不同。它不依赖系统调度,而是由代码主动让出执行权。
在 Lua 中,创建协同程序非常简单,只需要调用 coroutine.create() 函数。这个函数接收一个函数作为参数,返回一个协同程序对象。这个对象本身并不执行,它只是“待命”状态,等待被唤醒。
-- 创建一个协同程序,传入一个函数作为执行体
local co = coroutine.create(function()
print("协同程序开始执行")
coroutine.yield() -- 暂停执行,把控制权交还给主程序
print("协同程序恢复执行")
end)
print("主程序继续运行")
-- 输出:主程序继续运行
-- 启动协同程序
coroutine.resume(co)
-- 输出:协同程序开始执行
-- 再次恢复
coroutine.resume(co)
-- 输出:协同程序恢复执行
从这段代码可以看出,协同程序的执行是“分段”的。第一次 coroutine.resume() 触发函数执行,直到遇到 coroutine.yield() 时暂停。之后再次调用 resume,它就从暂停的地方继续往下执行。
这种“暂停-恢复”的特性,正是 Lua 协同程序最核心的价值所在。
协同程序的状态与生命周期
每个协同程序在运行过程中,会处于不同的状态。了解这些状态,有助于我们更好地控制协同程序的行为。
| 状态 | 说明 |
|---|---|
| suspended(挂起) | 协同程序刚刚创建,或执行中被 yield 暂停,等待恢复 |
| running(运行) | 协同程序正在执行,但不会主动改变状态,除非调用 yield |
| dead(死亡) | 协同程序执行完成,或者抛出未捕获的错误,不能再恢复 |
我们可以通过 coroutine.status() 函数来查询协同程序的当前状态。
local co = coroutine.create(function()
print("第一段执行")
coroutine.yield() -- 暂停
print("第二段执行")
end)
-- 初始状态是 suspended
print(coroutine.status(co)) -- 输出:suspended
coroutine.resume(co) -- 启动
print(coroutine.status(co)) -- 输出:suspended(再次被 yield 暂停)
coroutine.resume(co) -- 再次恢复
print(coroutine.status(co)) -- 输出:dead(执行完毕)
当协同程序执行完毕,resume 会返回 false 和错误信息。如果协同程序抛出错误,也会通过 resume 返回。
local co = coroutine.create(function()
print("开始执行")
error("出错了!")
end)
local success, msg = coroutine.resume(co)
print(success, msg) -- 输出:false 出错了!
状态管理是使用协同程序时的重要一环。尤其在处理多个协同程序时,需要判断它们是否还“活着”,避免对已结束的协同程序调用 resume。
用协同程序实现生产者-消费者模型
协同程序最经典的用途之一,就是实现“生产者-消费者”模型。它能有效解耦任务,让数据的生成与处理分离。
想象一下:一个任务负责生成数字,另一个任务负责处理这些数字。如果用普通函数,必须等生成完成才能处理,效率低下。但使用协同程序,我们可以让生成和处理交替进行。
-- 生产者:生成 1 到 5 的数字
local function producer()
for i = 1, 5 do
print("生产者生成:" .. i)
coroutine.yield(i) -- 将数字“产出”给消费者
end
end
-- 消费者:接收并处理数字
local function consumer()
local i = 1
while true do
local value = coroutine.yield() -- 从生产者获取数据
print("消费者处理:" .. value)
i = i + 1
if i > 5 then break end
end
end
-- 创建协同程序
local prod_co = coroutine.create(producer)
local cons_co = coroutine.create(consumer)
-- 启动消费者(首次 resume,让它进入等待状态)
coroutine.resume(cons_co)
-- 交替执行:生产者生成,消费者接收
while true do
local success, value = coroutine.resume(prod_co)
if not success then break end -- 生产完成,跳出
-- 把生产出的数据传给消费者
coroutine.resume(cons_co, value)
end
运行这段代码,输出如下:
生产者生成:1
消费者处理:1
生产者生成:2
消费者处理:2
...
生产者生成:5
消费者处理:5
这个例子中,coroutine.yield() 被用作“数据通道”。生产者调用 yield 时,把值返回给主程序,主程序再传给消费者;消费者调用 yield 时,会“等待”下一个值。整个过程像流水线一样顺畅。
协同程序与状态机的结合
协同程序非常适合实现状态机。在游戏开发、流程控制等场景中,状态机是常见模式。协同程序可以自然地表达状态切换,代码逻辑清晰,易于维护。
比如,实现一个简单的“用户登录流程”状态机:
local function login_state_machine()
print("开始登录流程")
-- 状态 1:输入用户名
print("请输入用户名")
local username = coroutine.yield("username_input")
print("用户名:" .. username)
-- 状态 2:输入密码
print("请输入密码")
local password = coroutine.yield("password_input")
print("密码已输入")
-- 状态 3:验证登录
print("正在验证...")
coroutine.yield("authenticating")
-- 状态 4:登录成功
print("登录成功!")
end
-- 创建协同程序
local login_co = coroutine.create(login_state_machine)
-- 模拟用户输入
local function simulate_input()
local success, state = coroutine.resume(login_co)
if not success then
print("登录流程结束")
return
end
if state == "username_input" then
-- 模拟用户输入
coroutine.resume(login_co, "alice")
elseif state == "password_input" then
coroutine.resume(login_co, "123456")
elseif state == "authenticating" then
-- 模拟验证耗时
coroutine.resume(login_co)
end
end
-- 启动流程
simulate_input()
-- 输出:开始登录流程
-- 请输入用户名
-- 用户名:alice
-- 请输入密码
-- 密码已输入
-- 正在验证...
-- 登录成功!
在这个例子中,每一步 yield 都是一个“等待点”,主程序根据当前状态,模拟用户输入并恢复协同程序。整个流程没有复杂的回调嵌套,也没有状态变量,逻辑非常直观。
协同程序的高级技巧:协程池与任务调度
在实际项目中,我们经常需要管理多个协同程序。这时可以构建一个“协程池”来统一调度。
以下是一个简单的任务调度器示例:
-- 任务队列
local tasks = {}
-- 添加任务
function add_task(task_func)
table.insert(tasks, coroutine.create(task_func))
end
-- 运行所有任务
function run_tasks()
local i = 1
while i <= #tasks do
local co = tasks[i]
local success, result = coroutine.resume(co)
if not success then
print("任务出错:" .. result)
-- 移除失败的任务
table.remove(tasks, i)
else
-- 如果任务已完成,也移除
if coroutine.status(co) == "dead" then
table.remove(tasks, i)
else
i = i + 1
end
end
end
end
-- 示例任务1:打印数字
local function task1()
for i = 1, 3 do
print("任务1:" .. i)
coroutine.yield()
end
end
-- 示例任务2:打印字母
local function task2()
for c = 'a', 'c' do
print("任务2:" .. c)
coroutine.yield()
end
end
-- 添加任务
add_task(task1)
add_task(task2)
-- 循环运行,直到所有任务完成
while #tasks > 0 do
run_tasks()
-- 可以加 sleep 模拟时间间隔
-- 但 Lua 本身没有 sleep,这里用空循环模拟
for i = 1, 1000000 do end
end
这个调度器实现了“多任务轮流执行”的效果。每个协同程序在 yield 时让出控制权,调度器遍历任务队列,依次恢复它们。这种设计适合轻量级并发场景,如游戏逻辑、异步 I/O 模拟等。
总结
Lua 协同程序(coroutine)是一种强大而灵活的编程工具。它不依赖操作系统线程,也不需要复杂的异步语法,而是通过“主动让出执行权”来实现协作式多任务。
从基础的 create、resume、yield,到生产者-消费者模型、状态机、协程池,协同程序的应用场景非常广泛。尤其在嵌入式系统、游戏脚本、配置引擎等对性能和内存敏感的领域,它几乎是首选方案。
它不像线程那样有上下文切换开销,也不像回调那样容易产生“回调地狱”。它的控制权完全掌握在开发者手中,逻辑清晰,调试方便。
如果你正在使用 Lua,建议尽早掌握协同程序的用法。它能让你的代码更具表现力,也更接近真实世界中“任务交替执行”的行为模式。
无论是初学者还是中级开发者,理解并熟练运用协同程序,都将为你的 Lua 编程之路打下坚实基础。