协程基础概念
协程(coroutine)是 Lua 中实现协作式多任务的核心机制。与操作系统线程不同,协程需要显式地让出执行权(yield),不能抢占式调度。这使得协程的调度完全由程序员控制,避免了线程同步的复杂性。
每个协程有四种状态:
local co = coroutine.create(function()
-- 协程体
end)
print(coroutine.status(co)) -- suspended(挂起)
coroutine.resume(co)
print(coroutine.status(co)) -- dead(死亡)
| 状态 | 说明 |
|---|---|
suspended | 挂起,等待恢复 |
running | 正在运行 |
normal | 已恢复其他协程 |
dead | 已执行完毕 |
协程的创建和运行
coroutine.create 接受一个函数作为协程体,返回一个协程对象(本质是一个线程类型的值):
local co = coroutine.create(function(a, b)
print("协程接收参数:", a, b)
local x = coroutine.yield("第一次yield", 42)
print("resume传入的值:", x)
local y = coroutine.yield("第二次yield")
print("再次resume传入的值:", y)
return "最终返回值"
end)
-- 第一次 resume,传入协程体函数的参数
local ok, val1, val2 = coroutine.resume(co, "hello", "world")
print(ok, val1, val2) -- true 第一次yield 42
-- 第二次 resume,传入的值作为 yield 的返回值
local ok, val = coroutine.resume(co, "from resume")
print(ok, val) -- true 第二次yield
-- 第三次 resume
local ok, val = coroutine.resume(co, "again")
print(ok, val) -- true 最终返回值
关键要点:
resume的额外参数传递给协程体函数或上一个yieldyield的参数作为resume的返回值resume的第一个返回值是布尔值,表示是否成功
yield 和 resume 的交互
协程通过 yield 和 resume 进行双向通信,这是 Lua 协程最核心的特性:
-- 生成器模式
local function fibonacci()
return coroutine.wrap(function()
local a, b = 0, 1
while true do
coroutine.yield(a)
a, b = b, a + b
end
end)
end
local fib = fibonacci()
for i = 1, 10 do
print(fib()) -- 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
end
coroutine.wrap 是 coroutine.create 的简化版本,它直接返回一个函数,调用该函数相当于执行 resume,且不返回成功标志。
生产者-消费者模式
协程是生产者-消费者问题的经典解决方案:
-- 生产者:生成数据
local function producer()
return coroutine.create(function()
for i = 1, 10 do
local data = string.format("数据-%d", i)
print("生产: " .. data)
coroutine.yield(data)
end
end)
end
-- 过滤器:转换数据
local function filter(prod)
return coroutine.create(function()
while true do
local ok, data = coroutine.resume(prod)
if not ok or data == nil then break end
local filtered = data:upper()
print("过滤: " .. filtered)
coroutine.yield(filtered)
end
end)
end
-- 消费者:使用数据
local function consumer(filt)
while true do
local ok, data = coroutine.resume(filt)
if not ok or data == nil then break end
print("消费: " .. data)
end
end
local prod = producer()
local filt = filter(prod)
consumer(filt)
协作式调度器
可以用协程实现简单的任务调度器:
local Scheduler = {}
Scheduler.__index = Scheduler
function Scheduler.new()
return setmetatable({
tasks = {},
running = nil
}, Scheduler)
end
function Scheduler:spawn(func)
local co = coroutine.create(func)
table.insert(self.tasks, co)
return co
end
function Scheduler:sleep(seconds)
-- 记录唤醒时间
coroutine.yield({type = "sleep", until = os.clock() + seconds})
end
function Scheduler:wait(event)
coroutine.yield({type = "wait", event = event})
end
function Scheduler:run()
while #self.tasks > 0 do
local i = 1
while i <= #self.tasks do
local co = self.tasks[i]
if coroutine.status(co) ~= "dead" then
self.running = co
local ok, result = coroutine.resume(co)
if not ok then
print("任务出错: " .. tostring(result))
table.remove(self.tasks, i)
elseif result and result.type == "sleep" then
-- 挂起直到指定时间
i = i + 1
else
-- 立即可再次运行
if coroutine.status(co) == "dead" then
table.remove(self.tasks, i)
else
i = i + 1
end
end
else
table.remove(self.tasks, i)
end
end
end
end
local sched = Scheduler.new()
sched:spawn(function()
for i = 1, 3 do
print("任务A 第" .. i .. "步")
coroutine.yield()
end
end)
sched:spawn(function()
for i = 1, 3 do
print("任务B 第" .. i .. "步")
coroutine.yield()
end
end)
sched:run()
用协程实现迭代器
协程可以极大地简化复杂迭代器的编写:
-- 树的深度优先遍历
local function tree_iter(tree)
return coroutine.wrap(function()
local function walk(node)
if node then
walk(node.left)
coroutine.yield(node.value)
walk(node.right)
end
end
walk(tree)
end)
end
-- 构造一棵二叉搜索树
local tree = {
value = 5,
left = {
value = 3,
left = {value = 1},
right = {value = 4}
},
right = {
value = 8,
left = {value = 6},
right = {value = 9}
}
}
for val in tree_iter(tree) do
print(val) -- 1, 3, 4, 5, 6, 8, 9
end
对比不用协程的版本,需要使用显式栈来模拟递归,代码量翻倍且可读性差得多。
协程实现异步 IO
在 OpenResty 等框架中,协程被用来将异步回调包装成同步代码:
-- 模拟异步 HTTP 请求
local function http_get(url)
-- 实际框架中这里会注册到事件循环
local co = coroutine.running()
-- 模拟异步回调
async_io(url, function(response)
coroutine.resume(co, response)
end)
return coroutine.yield()
end
-- 使用协程包装同步风格
local function handler()
local res1 = http_get("https://api.example.com/users")
print("获取用户:", res1)
local res2 = http_get("https://api.example.com/orders")
print("获取订单:", res2)
-- 两段请求看起来是顺序执行的,实际是非阻塞的
return {users = res1, orders = res2}
end
coroutine.wrap(handler)()
协程的错误处理
协程中未捕获的错误会导致 resume 返回 false 和错误信息:
local co = coroutine.create(function()
error("协程内部错误")
end)
local ok, err = coroutine.resume(co)
print(ok) -- false
print(err) -- 协程内部错误
-- 安全的协程包装
local function safe_resume(co, ...)
local results = {coroutine.resume(co, ...)}
if not results[1] then
print("协程错误: " .. tostring(results[2]))
return nil, results[2]
end
return table.unpack(results, 2)
end
注意事项
使用协程时需要注意以下要点:
- Lua 协程是协作式的,必须显式
yield才能让出控制权 yield只能在创建它的协程内部调用,不能在 C 函数或元方法中直接调用- 如果在 C 函数中需要使用
yield,应使用coroutine.wrap或确保调用链中没有 C 边界 - LuaJIT 的协程实现有额外的栈限制,深度嵌套的协程可能耗尽 C 栈
coroutine.yield不能跨越不同 Lua 状态(lua_State)调用- 协程的内存开销比线程小得多,可以轻松创建数万个协程
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。