Lua协程深入解析

深入理解Lua协程(coroutine)机制,掌握协作式多任务、生产者-消费者模式和异步编程实践。

协程基础概念

协程(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)

协作式调度器

可以用协程实现简单的任务调度器:

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

继续阅读

探索更多技术文章

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

全部文章 返回首页