协程
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 服务端 |
| luv | libuv 的 Lua 绑定 | 跨平台异步 I/O |
| async.lua | async/await 风格封装 | 通用异步编程 |
| Promise | Promise/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. 练习、验收与复跑范围
- 将事件循环中的两个任务各改为 yield 两次。验收:两个任务都执行到末尾,队列最终清空,执行顺序保持轮转。解题要点是挂起后入队,而不是创建新协程丢失原栈。
- 在第一个任务中抛错。验收:run 返回错误,不能继续输出“全部完成”;调度器必须检查 resume 的第一个结果。
- 让协程 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 不会让阻塞操作变成异步。掌握协程的使用方法,可以帮助开发者编写高性能的并发程序。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。