Lua 协程深入解析:协作式多任务的实现

深入理解 Lua 协程:create/resume/yield/status 的完整 API、协程四种状态与执行流转、生产者-消费者模式实现,以及协程与线程的本质区别和典型应用场景。

协程基础概念

协程(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 的额外参数传递给协程体函数或上一个 yield
  • yield 的参数作为 resume 的返回值
  • resume 的第一个返回值是布尔值,表示是否成功

yield 和 resume 的交互

协程通过 yieldresume 进行双向通信,这是 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.wrapcoroutine.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)

协作式调度器

可以用协程实现简单的任务调度器(Skynet 框架的 Actor 调度就建立在类似的协程模型之上):

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 等框架(参见 Lua 在 Web 开发中的应用)中,协程被用来将异步回调包装成同步代码:

-- 模拟异步 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)调用
  • 协程的内存开销比线程小得多,可以轻松创建数万个协程

常见问题(FAQ)

Lua 协程是线程吗?

不是。协程虽然拥有独立的栈和局部变量,但它运行在主线程之内,同一时刻只有一个协程在执行。协程的切换完全由 yield / resume 显式控制,不存在线程的抢占和加锁问题,创建和切换的开销也远小于操作系统线程。

协程能实现并行计算吗?

不能。单个 Lua 状态中的协程只能做到并发(concurrency)而无法并行(parallelism),任意时刻只有一个协程占用 CPU。要利用多核,需要采用多 Lua 状态配合多线程的方案,例如 Skynet 框架的多 Actor 模型。

协程里出错会怎样?

协程体内未捕获的错误不会让主程序崩溃,而是使 resume 返回 false 和错误信息,协程随即进入 dead 状态且无法再次恢复。需要注意 wrap 创建的协程行为不同:错误会直接向上传播,需要自行用 pcall 捕获。

coroutine.wrap 和 coroutine.create 有什么区别?

create 返回协程对象,需要用 resume 手动恢复,resume 的第一个返回值表示执行是否成功;wrap 则直接返回一个函数,调用它即恢复协程,但出错时会直接抛出错误而非返回 false。做迭代器时用 wrap 更简洁,需要检查错误时用 create 更稳妥。

相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「lua」更多文章