Lua 协同程序(coroutine)(建议收藏)

什么是 Lua 协同程序(coroutine)?

在编写程序时,我们常常遇到需要“暂停”某个任务,稍后再继续执行的场景。比如:下载一个大文件时,不想阻塞整个程序;或者处理多个任务,希望它们能轮流运行,而不是一个接一个地执行。这时候,传统的“函数调用”方式就显得力不从心了。

Lua 提供了一种强大的机制来解决这类问题——协同程序(coroutine)。它不是多线程,也不是异步回调,而是一种轻量级的并发模型。你可以把它想象成“可以暂停和恢复的小任务”,就像一个在执行中被打断的演员,可以随时从断点继续演出,而不需要从头开始。

协同程序的核心特点是:由程序员显式控制何时暂停、何时恢复。这与操作系统调度的线程完全不同。它不依赖系统调度,而是由代码主动让出执行权。

在 Lua 中,创建协同程序非常简单,只需要调用 coroutine.create() 函数。这个函数接收一个函数作为参数,返回一个协同程序对象。这个对象本身并不执行,它只是“待命”状态,等待被唤醒。

-- 创建一个协同程序,传入一个函数作为执行体
local co = coroutine.create(function()
    print("协同程序开始执行")
    coroutine.yield() -- 暂停执行,把控制权交还给主程序
    print("协同程序恢复执行")
end)

print("主程序继续运行")
-- 输出:主程序继续运行

-- 启动协同程序
coroutine.resume(co)
-- 输出:协同程序开始执行

-- 再次恢复
coroutine.resume(co)
-- 输出:协同程序恢复执行

从这段代码可以看出,协同程序的执行是“分段”的。第一次 coroutine.resume() 触发函数执行,直到遇到 coroutine.yield() 时暂停。之后再次调用 resume,它就从暂停的地方继续往下执行。

这种“暂停-恢复”的特性,正是 Lua 协同程序最核心的价值所在。


协同程序的状态与生命周期

每个协同程序在运行过程中,会处于不同的状态。了解这些状态,有助于我们更好地控制协同程序的行为。

状态 说明
suspended(挂起) 协同程序刚刚创建,或执行中被 yield 暂停,等待恢复
running(运行) 协同程序正在执行,但不会主动改变状态,除非调用 yield
dead(死亡) 协同程序执行完成,或者抛出未捕获的错误,不能再恢复

我们可以通过 coroutine.status() 函数来查询协同程序的当前状态。

local co = coroutine.create(function()
    print("第一段执行")
    coroutine.yield() -- 暂停
    print("第二段执行")
end)

-- 初始状态是 suspended
print(coroutine.status(co)) -- 输出:suspended

coroutine.resume(co) -- 启动
print(coroutine.status(co)) -- 输出:suspended(再次被 yield 暂停)

coroutine.resume(co) -- 再次恢复
print(coroutine.status(co)) -- 输出:dead(执行完毕)

当协同程序执行完毕,resume 会返回 false 和错误信息。如果协同程序抛出错误,也会通过 resume 返回。

local co = coroutine.create(function()
    print("开始执行")
    error("出错了!")
end)

local success, msg = coroutine.resume(co)
print(success, msg) -- 输出:false    出错了!

状态管理是使用协同程序时的重要一环。尤其在处理多个协同程序时,需要判断它们是否还“活着”,避免对已结束的协同程序调用 resume


用协同程序实现生产者-消费者模型

协同程序最经典的用途之一,就是实现“生产者-消费者”模型。它能有效解耦任务,让数据的生成与处理分离。

想象一下:一个任务负责生成数字,另一个任务负责处理这些数字。如果用普通函数,必须等生成完成才能处理,效率低下。但使用协同程序,我们可以让生成和处理交替进行。

-- 生产者:生成 1 到 5 的数字
local function producer()
    for i = 1, 5 do
        print("生产者生成:" .. i)
        coroutine.yield(i) -- 将数字“产出”给消费者
    end
end

-- 消费者:接收并处理数字
local function consumer()
    local i = 1
    while true do
        local value = coroutine.yield() -- 从生产者获取数据
        print("消费者处理:" .. value)
        i = i + 1
        if i > 5 then break end
    end
end

-- 创建协同程序
local prod_co = coroutine.create(producer)
local cons_co = coroutine.create(consumer)

-- 启动消费者(首次 resume,让它进入等待状态)
coroutine.resume(cons_co)

-- 交替执行:生产者生成,消费者接收
while true do
    local success, value = coroutine.resume(prod_co)
    if not success then break end -- 生产完成,跳出

    -- 把生产出的数据传给消费者
    coroutine.resume(cons_co, value)
end

运行这段代码,输出如下:

生产者生成:1
消费者处理:1
生产者生成:2
消费者处理:2
...
生产者生成:5
消费者处理:5

这个例子中,coroutine.yield() 被用作“数据通道”。生产者调用 yield 时,把值返回给主程序,主程序再传给消费者;消费者调用 yield 时,会“等待”下一个值。整个过程像流水线一样顺畅。


协同程序与状态机的结合

协同程序非常适合实现状态机。在游戏开发、流程控制等场景中,状态机是常见模式。协同程序可以自然地表达状态切换,代码逻辑清晰,易于维护。

比如,实现一个简单的“用户登录流程”状态机:

local function login_state_machine()
    print("开始登录流程")

    -- 状态 1:输入用户名
    print("请输入用户名")
    local username = coroutine.yield("username_input")
    print("用户名:" .. username)

    -- 状态 2:输入密码
    print("请输入密码")
    local password = coroutine.yield("password_input")
    print("密码已输入")

    -- 状态 3:验证登录
    print("正在验证...")
    coroutine.yield("authenticating")

    -- 状态 4:登录成功
    print("登录成功!")
end

-- 创建协同程序
local login_co = coroutine.create(login_state_machine)

-- 模拟用户输入
local function simulate_input()
    local success, state = coroutine.resume(login_co)
    if not success then
        print("登录流程结束")
        return
    end

    if state == "username_input" then
        -- 模拟用户输入
        coroutine.resume(login_co, "alice")
    elseif state == "password_input" then
        coroutine.resume(login_co, "123456")
    elseif state == "authenticating" then
        -- 模拟验证耗时
        coroutine.resume(login_co)
    end
end

-- 启动流程
simulate_input()
-- 输出:开始登录流程
--       请输入用户名
--       用户名:alice
--       请输入密码
--       密码已输入
--       正在验证...
--       登录成功!

在这个例子中,每一步 yield 都是一个“等待点”,主程序根据当前状态,模拟用户输入并恢复协同程序。整个流程没有复杂的回调嵌套,也没有状态变量,逻辑非常直观。


协同程序的高级技巧:协程池与任务调度

在实际项目中,我们经常需要管理多个协同程序。这时可以构建一个“协程池”来统一调度。

以下是一个简单的任务调度器示例:

-- 任务队列
local tasks = {}

-- 添加任务
function add_task(task_func)
    table.insert(tasks, coroutine.create(task_func))
end

-- 运行所有任务
function run_tasks()
    local i = 1
    while i <= #tasks do
        local co = tasks[i]
        local success, result = coroutine.resume(co)

        if not success then
            print("任务出错:" .. result)
            -- 移除失败的任务
            table.remove(tasks, i)
        else
            -- 如果任务已完成,也移除
            if coroutine.status(co) == "dead" then
                table.remove(tasks, i)
            else
                i = i + 1
            end
        end
    end
end

-- 示例任务1:打印数字
local function task1()
    for i = 1, 3 do
        print("任务1:" .. i)
        coroutine.yield()
    end
end

-- 示例任务2:打印字母
local function task2()
    for c = 'a', 'c' do
        print("任务2:" .. c)
        coroutine.yield()
    end
end

-- 添加任务
add_task(task1)
add_task(task2)

-- 循环运行,直到所有任务完成
while #tasks > 0 do
    run_tasks()
    -- 可以加 sleep 模拟时间间隔
    -- 但 Lua 本身没有 sleep,这里用空循环模拟
    for i = 1, 1000000 do end
end

这个调度器实现了“多任务轮流执行”的效果。每个协同程序在 yield 时让出控制权,调度器遍历任务队列,依次恢复它们。这种设计适合轻量级并发场景,如游戏逻辑、异步 I/O 模拟等。


总结

Lua 协同程序(coroutine)是一种强大而灵活的编程工具。它不依赖操作系统线程,也不需要复杂的异步语法,而是通过“主动让出执行权”来实现协作式多任务。

从基础的 createresumeyield,到生产者-消费者模型、状态机、协程池,协同程序的应用场景非常广泛。尤其在嵌入式系统、游戏脚本、配置引擎等对性能和内存敏感的领域,它几乎是首选方案。

它不像线程那样有上下文切换开销,也不像回调那样容易产生“回调地狱”。它的控制权完全掌握在开发者手中,逻辑清晰,调试方便。

如果你正在使用 Lua,建议尽早掌握协同程序的用法。它能让你的代码更具表现力,也更接近真实世界中“任务交替执行”的行为模式。

无论是初学者还是中级开发者,理解并熟练运用协同程序,都将为你的 Lua 编程之路打下坚实基础。