第 13 章 协程
第 13 章 协程
协程允许函数在保留当前状态的同时暂停和恢复。它适合生成器、状态机、流式处理和需要把复杂异步流程写成顺 code 的场景。
协程模型
Lua 协程是协作式的:只有显式 yield 或结束时才会切换,不会像操作系统线程一样被抢占。协程之间默认不并行,也不会自动利用多核 CPU。
一个协程从 suspended 状态开始,经过 resume 运行,在 yield 处暂停,最终进入 dead 状态。
create、resume 与 yield
local worker = coroutine.create(function(name)
local value = coroutine.yield("hello " .. name)
print("收到:", value)
return "done"
end)
print(coroutine.status(worker)) -- suspended
local ok, message = coroutine.resume(worker, "Lua")
print(ok, message) -- true hello Lua
local finished, result = coroutine.resume(worker, "继续")
print(finished, result) -- true done
print(coroutine.status(worker)) -- dead
resume 返回是否成功,以及 yield 的值或最终返回值。yield 返回下一次 resume 传入的参数。
wrap
coroutine.wrap 返回一个普通函数,调用时自动 resume。错误会直接向调用方抛出:
local generate = coroutine.wrap(function()
for index = 1, 3 do
coroutine.yield(index * 10)
end
end)
print(generate()) -- 10
print(generate()) -- 20
print(generate()) -- 30
需要显式处理错误时使用 create 和 resume;生成器场景使用 wrap 更简洁。
状态与关闭
print(coroutine.isyieldable())
local running, isMain = coroutine.running()
print(type(running), isMain)
Lua 5.4 提供 coroutine.close。关闭一个挂起或正常结束的协程,会执行它的待关闭资源:
local function task()
local handle <close> = setmetatable({}, {
__close = function()
print("清理资源")
end
})
coroutine.yield()
end
local coroutineValue = coroutine.create(task)
coroutine.resume(coroutineValue)
print(coroutine.close(coroutineValue)) -- true
不能关闭正在运行的协程。close 也可能返回失败和错误对象。
生产者与消费者
协程可以把“逐个产生值”的逻辑写成普通循环:
local function producer(items)
return coroutine.create(function()
for _, item in ipairs(items) do
coroutine.yield(item)
end
end)
end
local source = producer({ "a", "b", "c" })
while true do
local ok, item = coroutine.resume(source)
if not ok then
error(item)
end
if item == nil then
break
end
print("消费:", item)
end
如果数据源是文件,可以在协程中逐行读取,消费者只看到值序列。这样读取细节与业务处理可以分离。
双向通信
resume 和 yield 都能传值:
local processor = coroutine.create(function()
while true do
local input = coroutine.yield()
if input == nil then
break
end
print("处理:", input:upper())
end
end)
coroutine.resume(processor)
coroutine.resume(processor, "lua")
coroutine.resume(processor, "coroutine")
coroutine.resume(processor)
第一次 resume 只启动协程,直到遇到 yield。之后的参数才成为挂起点 yield 的返回值。
状态机与异步流程
协程可以让状态机按顺序表达:
local function workflow()
print("1. 读取输入")
local input = coroutine.yield("waiting-input")
print("2. 校验输入")
if not input or input == "" then
return nil, "empty input"
end
print("3. 保存结果")
return "saved"
end
local process = coroutine.create(workflow)
print(coroutine.resume(process))
print(coroutine.resume(process, "hello"))
在事件驱动程序中,调度器可以在 I/O 完成时恢复对应协程。这能把回调链写成顺序代码,但调度器仍需处理超时、取消、并发上限和异常。
简单协程调度
local jobs = {}
local function schedule(job)
jobs[#jobs + 1] = coroutine.create(job)
end
local function run()
while #jobs > 0 do
local job = table.remove(jobs, 1)
local ok, message = coroutine.resume(job)
if not ok then
print("job failed:", message)
elseif coroutine.status(job) ~= "dead" then
jobs[#jobs + 1] = job
end
end
end
schedule(function()
coroutine.yield()
print("first")
end)
schedule(function()
print("second")
end)
run()
这个调度器会空转,不能替代真正的 I/O 事件循环。生产程序应使用成熟的异步框架或宿主事件循环。
协程使用边界
-
协程不是线程,不能自动并行执行 CPU 密集任务。
-
不要在协程之间共享可变状态而没有明确同步策略。
-
捕获错误并关闭未完成协程,避免资源泄漏。
-
避免在库的每个函数中隐藏协程切换,调用者应能预测控制流。
-
主线程中调用
coroutine.yield会失败,除非当前函数处于可让出环境。