《Lua快速入门》8.协程

Lua 提供了协程(Coroutine)的支持,允许程序在单线程内实现并发执行。协程的切换由程序显式控制,而不是由操作系统调度,因此开销更小。

协程

1. 协程的基本概念

1.1 什么是协程?

Lua 协程是由程序显式恢复、可主动挂起的执行栈。同一 Lua 状态中的协程不会因此在多个 CPU 核上同时执行;协程切换的具体成本需要按解释器、宿主和调用栈测量。

1.2 协程的特点

  • 轻量级:每个任务保留自己的执行栈;内存与切换成本受运行环境影响。
  • 协作式调度:协程的切换由程序显式控制,而不是由操作系统调度。
  • 非抢占式:协程执行到 yield,或返回/出错后才交回控制;长计算若不 yield 会占用当前线程。

1.3 协程与线程的对比

特性协程线程
调度方式协作式抢占式
切换开销低高
并发性单线程内并发多线程并发
适用场景需要显式挂起/恢复的任务;异步 I/O 还需宿主支持需要并行或独立阻塞执行的任务

2. 协程的创建与切换

2.1 协程的创建

Lua 使用 coroutine.create 函数创建协程,返回一个协程对象。

local co = coroutine.create(function()
    print("Hello from coroutine!")
end)

2.2 协程的启动与恢复

使用 coroutine.resume 启动或恢复协程的执行。

coroutine.resume(co)  -- 输出: Hello from coroutine!

2.3 协程的挂起

使用 coroutine.yield 挂起协程的执行,并返回一个值。

local co = coroutine.create(function()
    print("Start")
    coroutine.yield("Paused")
    print("End")
end)

print(coroutine.resume(co))  -- 先输出 Start,再输出 true 和 Paused(两行)
print(coroutine.resume(co))  -- 先输出 End,再输出 true(两行)

2.4 协程的状态

使用 coroutine.status 检查协程的状态。

print(coroutine.status(co))  -- 承接上例两次 resume 后输出: dead

2.5 协程的返回值

协程可以通过 coroutine.yield 和 coroutine.resume 传递值。

local co = coroutine.create(function()
    local x = coroutine.yield(10)
    print("Received:", x)
end)

local _, value = coroutine.resume(co)
print("Yielded:", value)  -- 输出: Yielded: 10
coroutine.resume(co, 20)  -- 输出: Received: 20

3. 协程在并发编程中的应用

3.1 协程与 I/O 操作

协程可表达“发起操作—等待—继续处理”的流程,但普通阻塞文件/网络调用仍会阻塞当前执行线程。下面只模拟挂起与恢复,不执行真实异步 I/O。

3.1.1 模拟异步 I/O
local function beginMockIO(callback)
    local co = coroutine.create(callback)
    local ok, waiting = coroutine.resume(co)
    assert(ok, waiting)
    return co
end

local pending = beginMockIO(function()
    print("Start I/O")
    coroutine.yield("waiting")
    print("End I/O")
end)
assert(coroutine.status(pending) == "suspended")
-- 用手动恢复模拟“就绪回调”;真实系统要由事件循环触发。
assert(coroutine.resume(pending))
assert(coroutine.status(pending) == "dead")

3.2 协程与事件循环

协程可以与事件循环结合,实现高效的并发编程。

3.2.1 简单事件循环
local tasks = {}

local function schedule(task)
    table.insert(tasks, task)
end

local function run()
    while #tasks > 0 do
        local task = table.remove(tasks, 1)
        local ok, err = coroutine.resume(task)
        if not ok then
            error(debug.traceback(task, tostring(err)), 0)
        end
        if coroutine.status(task) == "suspended" then
            schedule(task) -- yield 后回到队尾,下一轮继续执行
        end
    end
end

schedule(coroutine.create(function()
    print("Task 1")
    coroutine.yield()
    print("Task 1 continued")
end))

schedule(coroutine.create(function()
    print("Task 2")
end))

run()

3.3 协程与生产者-消费者模型

协程可以用于实现生产者-消费者模型,解决生产者和消费者之间的同步问题。

3.3.1 生产者-消费者示例
local queue = {}
local maxSize = 5

local function producer()
    for i = 1, 10 do
        while #queue >= maxSize do
            coroutine.yield()
        end
        table.insert(queue, i)
        print("Produced:", i)
    end
end

local function consumer()
    for i = 1, 10 do
        while #queue == 0 do
            coroutine.yield()
        end
        local item = table.remove(queue, 1)
        print("Consumed:", item)
    end
end

local p = coroutine.create(producer)
local c = coroutine.create(consumer)

while coroutine.status(p) ~= "dead" or coroutine.status(c) ~= "dead" do
    for _, co in ipairs({ p, c }) do
        if coroutine.status(co) ~= "dead" then
            local ok, err = coroutine.resume(co)
            assert(ok, err)
        end
    end
end

3.4 协程与网络编程

下例需要另外安装 LuaSocket,保留它是为了辨认阻塞陷阱:receive 没有挂起到事件循环,第一次 resume 会一直处理当前连接,返回后才 accept 下一个。它是串行阻塞反例,不是多连接并发实现;本节不把这个服务器列入可直接运行的标准库示例。真正的协程网络调度还需要非阻塞 socket、就绪通知、超时与分段收发处理。

3.4.1 阻塞反例:包装协程不会自动产生连接并发
local socket = require("socket")

local function handleConnection(client)
    local co = coroutine.create(function()
        while true do
            local data = client:receive()
            if not data then break end
            print("Received:", data)
            client:send("Echo: " .. data .. "\n")
        end
        client:close()
    end)
    coroutine.resume(co)
end

local server = assert(socket.bind("127.0.0.1", 8080))
while true do
    local client = server:accept()
    handleConnection(client)
end

4. 协程状态机与调度原理

协程本质是可挂起的函数调用栈。理解其状态转换有助于编写健壮的异步代码:

suspended --resume--> running --yield--> suspended
running --resume(子协程)--> normal --子协程让出或结束--> running
running --return / error--> dead
状态含义可执行操作
suspended新建或 yield 后挂起resume
running正在执行yield / return
normal当前协程等待它恢复的子协程让出或结束此时不能再次 resume 这个父协程
dead函数体执行完毕或出错不可 resume(会返回 false + 错误)

关键陷阱:resume 一个 dead 协程不会报错抛出异常,而是返回 (false, "cannot resume dead coroutine")。调用方必须检查返回值:

local ok, result = coroutine.resume(co)
if not ok then
    print("Coroutine error:", result)  -- result 是错误信息
end

5. 协程与回调的比较

传统的异步编程使用回调函数,代码呈"回调地狱":

-- 回调式异步(混乱)
fetch("url1", function(data1)
    fetch("url2", function(data2)
        fetch("url3", function(data3)
            process(data1, data2, data3)
        end)
    end)
end)

-- 协程式异步(线性直观)
coroutine.wrap(function()
    local data1 = fetch_async("url1")
    local data2 = fetch_async("url2")
    local data3 = fetch_async("url3")
    process(data1, data2, data3)
end)()

协程让异步代码读起来像同步代码,显著提升可维护性。

6. 实际框架:从协程到 async/await

异步库会在协程之外提供事件循环与 I/O 适配。以下链接供继续查阅;接入时需按各库的版本和文档确认 API,本节不将它们计入标准库复跑:

框架/库封装方式适用场景
Copas协程调度器 + 网络事件TCP/HTTP 服务端
luvlibuv 的 Lua 绑定跨平台异步 I/O
async.luaasync/await 风格封装通用异步编程
PromisePromise/A+ 实现链式异步操作

下面只示意 await 需要的宿主契约;http_get、parse_links 和 async.all 没有在本节实现,不是某个库已经验证的调用方式。

-- 概念片段:需要另外实现/接入异步宿主,不能独立运行
local async = require("async")

async(function(await)
    local html = await(http_get("https://example.com"))
    local links = parse_links(html)
    
    -- 并发请求所有链接
    local tasks = {}
    for _, link in ipairs(links) do
        table.insert(tasks, http_get(link))
    end
    local pages = await(async.all(tasks))
    
    print("Fetched", #pages, "pages")
end)

7. 协程调试技巧

问题诊断方法解决
协程卡死检查所有 yield 是否有对应的 resume确保事件循环不阻塞
任务意外滞留检查队列、回调与注册表持有的引用取消任务后解除这些引用,外部资源按宿主协议关闭
状态混乱print(coroutine.status(co))添加状态断言
错误吞没始终检查 resume 返回值 ok包装 resume 统一错误处理
-- 安全的 resume 包装
function safe_resume(co, ...)
    local results = table.pack(coroutine.resume(co, ...))
    if not results[1] then
        io.stderr:write(debug.traceback(co, tostring(results[2])), "\n")
    end
    -- 保留多个结果以及其中的 nil,不把 yield/return 截成一个值。
    return table.unpack(results, 1, results.n)
end

8. 性能考量

维度协程操作系统线程
创建/栈内存按 Lua 构建、栈深与对象引用实测按操作系统和线程栈配置实测
切换开销按任务负载与挂起位置测量按调度、系统调用和负载测量
可承载数量由内存、调度与等待资源决定由内存、调度与系统限制决定
并行能力无(单线程)多核并行
阻塞影响阻塞所在的 Lua 执行线程阻塞调用所在线程

Lua 协程是**并发(concurrency)而非并行(parallelism)**的工具。如需多核并行,请使用多进程(如 OpenResty 的 worker process)或将计算密集型任务交给 C 扩展。

9. 练习、验收与复跑范围

  1. 将事件循环中的两个任务各改为 yield 两次。验收:两个任务都执行到末尾,队列最终清空,执行顺序保持轮转。解题要点是挂起后入队,而不是创建新协程丢失原栈。
  2. 在第一个任务中抛错。验收:run 返回错误,不能继续输出“全部完成”;调度器必须检查 resume 的第一个结果。
  3. 让协程 yield(“a”, nil, “c”),再返回两个值。验收:safe_resume 保留结果数量与中间的 nil,可用 table.pack 检查 n。

2026-10-17 在临时构建的 PUC Lua 5.4.7 / 5.5.1 复跑状态转换、模拟 I/O、轮转、生产/消费、错误路径与多返回值。未执行 LuaSocket 服务器、引擎宿主或第三方 async API,不据此给它们兼容性认证。状态与 resume 的定义见 Lua 5.4 官方协程文档 。

10. 总结

协程是 Lua 中一种强大的并发编程工具,具有轻量级、协作式调度的特点。配合明确的调度器及非阻塞 I/O 适配,协程可组织事件循环、生产者-消费者和网络任务;单独包装 coroutine 不会让阻塞操作变成异步。掌握协程的使用方法,可以帮助开发者编写高性能的并发程序。


继续阅读

探索更多技术文章

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

全部文章 返回首页

「lua」更多文章

  1. Lua 时间日期处理与时区
  2. Lua 在嵌入式与 IoT 中的开发实践
  3. Lua 与 WebAssembly 互操作