协程详解
Lua协程与多任务
概述
Lua 的协程(coroutine)是一种非抢占式的多任务机制,通过协作式调度实现并发。协程可以暂停执行(yield)和恢复执行(resume),非常适合实现状态机、生成器和协作式多任务。
基础概念
协程核心 API
| 函数 | 说明 |
|---|---|
| coroutine.create | 创建协程,返回 thread 类型 |
| coroutine.resume | 恢复协程执行 |
| coroutine.yield | 挂起当前协程 |
| coroutine.wrap | 创建协程,返回可调用的函数 |
| coroutine.status | 获取协程状态 |
| coroutine.running | 获取当前运行的协程 |
协程状态
| 状态 | 说明 |
|---|---|
| suspended | 挂起,等待 resume |
| running | 正在执行 |
| normal | 恢复了其他协程 |
| dead | 执行完毕或出错 |
快速上手
创建和运行协程
-- 方式一:create + resume
local co = coroutine.create(function()
for i = 1, 5 do
print("协程输出: " .. i)
coroutine.yield(i)
end
end)
-- resume 返回:是否成功, yield 传入的值
local ok, value = coroutine.resume(co) -- true 1
print(coroutine.status(co)) -- suspended
-- 继续执行
ok, value = coroutine.resume(co) -- true 2
-- 方式二:wrap(更简洁)
local co2 = coroutine.wrap(function()
for i = 1, 5 do
coroutine.yield(i)
end
end)
print(co2()) -- 1(直接调用,不需要处理 ok)
print(co2()) -- 2
resume 和 yield 的数据传递
-- resume 向 yield 传值
local co = coroutine.create(function()
local received = coroutine.yield("hello") -- 第一次 yield
print("收到: " .. received) -- 输出 "收到: world"
return "done"
end)
local ok, msg = coroutine.resume(co) -- msg = "hello"
coroutine.resume(co, "world") -- 传递 "world" 给 yield
-- yield 向 resume 传值
local co2 = coroutine.create(function(a, b) -- resume 传入的参数
coroutine.yield(a + b) -- yield 返回 a+b
coroutine.yield(a * b) -- yield 返回 a*b
end)
local ok, result = coroutine.resume(co2, 3, 4) -- result = 7
ok, result = coroutine.resume(co2) -- result = 12
详细用法
生产者-消费者模式
-- 生产者
function producer()
return coroutine.wrap(function()
while true do
local value = io.read()
if not value then return end
coroutine.yield(value)
end
end)
end
-- 消费者
function consumer(prod)
for value in prod do
print("消费: " .. value)
end
end
-- 过滤器
function filter(prod, predicate)
return coroutine.wrap(function()
for value in prod do
if predicate(value) then
coroutine.yield(value)
end
end
end)
end
-- 使用管道
local prod = producer()
local filtered = filter(prod, function(v) return #v > 3 end)
consumer(filtered)
状态机
-- 使用协程实现状态机
function trafficLight()
return coroutine.wrap(function()
local state = "red"
local durations = { red = 5, green = 4, yellow = 1 }
local transitions = { red = "green", green = "yellow", yellow = "red" }
while true do
coroutine.yield(state, durations[state])
state = transitions[state]
end
end)
end
local light = trafficLight()
for i = 1, 10 do
local state, duration = light()
print(string.format("信号灯: %s (持续 %d 秒)", state, duration))
end
协程实现异步回调
-- 将回调式 API 转换为同步式调用
function asyncToSync(asyncFn, ...)
local co = coroutine.running()
local args = { ... }
asyncFn(unpack(args), function(result)
coroutine.resume(co, result)
end)
return coroutine.yield()
end
-- 使用
function fetchData(url, callback)
-- 模拟异步操作
setTimeout(function()
callback("数据来自: " .. url)
end, 1000)
end
-- 同步式调用
local co = coroutine.wrap(function()
local data = asyncToSync(fetchData, "https://example.com")
print(data)
end)
co()
常见场景
生成器模式
-- 斐波那契数列生成器
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
-- 素数生成器
function primes()
return coroutine.wrap(function()
local n = 2
while true do
local isPrime = true
for i = 2, math.floor(math.sqrt(n)) do
if n % i == 0 then isPrime = false; break end
end
if isPrime then coroutine.yield(n) end
n = n + 1
end
end)
end
local p = primes()
for i = 1, 10 do print(p()) end
任务调度器
-- 简单的协程调度器
local scheduler = {
tasks = {},
current = nil
}
function scheduler.spawn(fn)
local co = coroutine.wrap(fn)
table.insert(scheduler.tasks, co)
end
function scheduler.yield()
coroutine.yield()
end
function scheduler.run()
while #scheduler.tasks > 0 do
local i = 1
while i <= #scheduler.tasks do
local co = scheduler.tasks[i]
if coroutine.status(co) == "dead" then
table.remove(scheduler.tasks, i)
else
co()
i = i + 1
end
end
end
end
-- 使用
scheduler.spawn(function()
for i = 1, 3 do
print("任务A: " .. i)
scheduler.yield()
end
end)
scheduler.spawn(function()
for i = 1, 3 do
print("任务B: " .. i)
scheduler.yield()
end
end)
scheduler.run()
-- 交替输出: A1, B1, A2, B2, A3, B3
注意事项
- Lua 协程是非抢占式的,必须主动 yield 才能切换
- resume 只能在主线程中调用,不能在协程中 resume 另一个非 suspended 的协程
- 协程出错时 resume 返回 false 和错误信息
- wrap 创建的协程出错时会直接抛出异常
- 协程不是线程,不提供真正的并行执行
- 避免在协程中执行长时间不 yield 的操作,会阻塞整个程序
进阶用法
协程池
-- 协程池:复用协程对象
local Pool = {}
Pool.__index = Pool
function Pool.new()
return setmetatable({ workers = {} }, Pool)
end
function Pool:submit(fn)
local co = table.remove(self.workers) or coroutine.create(function(task)
-- 循环执行任务
while task do
task()
task = coroutine.yield() -- 执行完毕,等待下一个任务
end
end)
coroutine.resume(co, fn) -- 传入任务
table.insert(self.workers, co) -- 放回池中
end
-- 使用
local pool = Pool.new()
pool:submit(function() print("任务1") end)
pool:submit(function() print("任务2") end)