协程非抢占式调度
Lua协程非抢占式调度详解。
协程非抢占式调度:Lua 的协作式并发模型
本文档对标 MIT 6.005(Software Construction)、Stanford CS110L(Principles of Computer Systems)、CMU 15-440(Distributed Systems)教学水准,系统剖析 Lua coroutine 机制的设计哲学、形式化语义、底层实现与工程实践。
0. 学习目标(Bloom 分类法)
完成本章节学习后,学习者应能够:
0.1 Remember(记忆)
- R1 列举 coroutine 的四个核心 API:
coroutine.create、coroutine.resume、coroutine.yield、coroutine.status。 - R2 复述 coroutine 的四种状态:
suspended、running、normal、dead。 - R3 陈述协作式(cooperative)与抢占式(preemptive)调度的差异:是否显式让出控制权。
0.2 Understand(理解)
- U1 解释 coroutine 与 thread(OS 线程)的本质差异:调度机制、并发性、共享内存。
- U2 阐述
coroutine.resume与coroutine.yield的”双向通信”机制:参数与返回值的传递。 - U3 解释非对称协程(asymmetric coroutine)与对称协程(symmetric coroutine)的区别:调用栈结构。
0.3 Apply(应用)
- A1 实现一个生产者-消费者模型,使用 coroutine 替代共享队列。
- A2 编写一个生成器(generator),延迟生成无穷序列。
- A3 利用 coroutine 实现状态机:模拟游戏循环、协议解析器。
0.4 Analyze(分析)
- An1 分析 coroutine 与 callback 在异步编程中的差异:可读性、调试、性能。
- An2 对比 Lua coroutine、Python generator、JavaScript async/await、Go goroutine 的设计取舍。
- An3 剖析 Lua 5.3 中
coroutine的内部实现:lua_State嵌套与lua_yield的 longjmp 机制。
0.5 Evaluate(评价)
- E1 评估在何种场景下应使用 coroutine 而非 OS 线程(I/O 密集、CPU 密集)。
- E2 评价 Lua 不支持对称协程的影响:是否限制了并发模型的表达能力。
- E3 判断 Lua 5.4 中
coroutine.close的引入对资源管理的意义。
0.6 Create(创造)
- C1 设计一个基于 coroutine 的异步 I/O 框架,模拟 Node.js 的事件循环。
- C2 实现一个对称协程库(基于 Lua 的非对称原语)。
- C3 构建一个 coroutine 调试器,可视化状态转换与数据流。
1. 历史动机与发展脉络
1.1 协程概念起源(1958)
协程(coroutine)一词由 Melvin Conway 于 1958 年提出,用于描述 COBOL 编译器的词法分析与语法分析模块的协作。Conway 将其定义为”一种可以暂停与恢复的子程序”。
与子程序(subroutine)的差异:
- 子程序:调用者主导,被调用者返回后控制权交回。
- 协程:双方平等,可双向让出与恢复。
1.2 Lua 1.0(1993):无协程支持
Lua 1.0 基于 SCOOPS(Smalltalk 风格 OOP),未提供 coroutine 原语。并发需求通过 closure 与 callback 满足。
1.3 Lua 2.x(1995):实验性协程
Lua 2.x 内部尝试过 coroutine 原语,但未对外发布。当时 PUC-Rio 团队主要关注语言核心稳定性。
1.4 Lua 5.0(2003):coroutine 正式引入
Lua 5.0 标志性引入 coroutine 标准库:
local co = coroutine.create(function(a, b)
print("start:", a, b)
local c = coroutine.yield(a + b)
print("resumed:", c)
return "done"
end)
coroutine.resume(co, 1, 2) -- start: 1 2; returns 3
coroutine.resume(co, 10) -- resumed: 10; returns "done"
设计基于非对称协程(asymmetric coroutine):
resume:调用者 → 被调用者。yield:被调用者 → 调用者。
1.5 Lua 5.1(2006):工业标准
Lua 5.1 的 coroutine API 稳定,被 WoW、Adobe Lightroom、Nginx/OpenResty 大量使用:
coroutine.create/coroutine.resume/coroutine.yield/coroutine.status/coroutine.wrap/coroutine.running。
重要限制:
- 不能在 C 函数中 yield(5.1 通过
lua_yield的 longjmp 限制)。 - 主线程(main thread)不能 yield。
1.6 Lua 5.2(2012):yield 跨 C 边界
Lua 5.2 改进:允许 coroutine.yield 跨越 C 函数边界:
/* Lua 5.2+ */
static int my_func(lua_State *L) {
/* 调用 Lua 函数,可能 yield */
lua_callk(L, 0, 0, ctx, k_function);
/* k_function 在 resume 后被调用 */
return 0;
}
新增 continuation-passing 风格的 lua_callk / lua_pcallk,支持 yield-resume 后的回调。
1.7 Lua 5.3(2015):性能优化
Lua 5.3 对 coroutine 的内部实现优化:
lua_State复用改进,减少创建开销。- yield-resume 的数据传递路径优化。
1.8 Lua 5.4(2020):coroutine.close
Lua 5.4 引入 coroutine.close:
local co = coroutine.create(function()
-- ...
coroutine.yield()
-- 未执行的部分
end)
coroutine.close(co) -- 显式关闭,标记为 dead
允许显式释放未完成的 coroutine 资源。配合 <close> 变量,coroutine 可作为确定性资源。
1.9 LuaJIT 与扩展
LuaJIT 保持 Lua 5.1 语义,但通过 FFI 与 ffi.C 提供 OS 线程支持。OpenResty 在 LuaJIT 之上构建 ngx.thread API,实现协作式与抢占式混合并发。
1.10 设计哲学总结
PUC-Rio 团队在《Coroutines in Lua》论文中阐明 coroutine 的设计原则:
- 简单性:4 个核心函数,无复杂调度器。
- 协作式:避免抢占式的竞态条件与同步开销。
- 栈式实现:每个 coroutine 拥有独立栈,可被暂停与恢复。
- 非对称:调用者主导,符合子程序心智模型。
2. 形式化定义
2.1 Lua Reference Manual 权威定义
Coroutines — Lua supports coroutines, also called collaborative multithreading. A coroutine in Lua represents an independent thread of execution. Unlike threads in multithread systems, however, a coroutine only suspends its execution by explicitly calling a yield function.
—— Lua 5.4 Reference Manual, §2.6 Coroutines
形式化定义:
其中:
- :coroutine 的执行状态(局部变量、PC)。
- :独立的调用栈。
- 。
2.2 状态转换
coroutine 的状态转换图:
resume()
┌────────────┐ ────────────> ┌────────────┐
│ suspended │ │ running │
└────────────┘ <──────────── └────────────┘
^ ▲ yield() │
│ │ │
│ └─────── return │
│ v
│ ┌────────────┐
└────────────────────────│ dead │
close() └────────────┘
形式化:
2.3 resume 与 yield 的双向通信
coroutine.resume(co, ...) 的语义:
coroutine.yield(...) 的语义:
双向数据流:
主协程 被调协程
│ │
│── resume(a, b) ──>│
│ │ (执行)
│<── yield(c, d) ───│
│ │ (暂停)
│── resume(e, f) ──>│
│ │ (恢复,yield 返回 e, f)
│<── return(g) ─────│
│ │ (dead)
2.4 非对称 vs 对称协程
非对称协程(Lua 默认):
调用关系形成栈:每次 resume 嵌套在 yield 中。
对称协程:
无调用者-被调用者关系,coroutine 之间平等转移控制权。
Lua 通过非对称原语可模拟对称协程(见 §7.3)。
2.5 协作式调度形式化
设 为 coroutine 集合,调度函数 :
关键约束:coroutine 必须显式 yield,否则其他 coroutine 永远无法运行(饥饿)。
2.6 coroutine.wrap 的形式化
wrap 返回一个函数,调用时自动 resume。若 coroutine 出错,wrap 抛出错误(而非返回 false)。
3. 理论推导与原理解析
3.1 coroutine 的内部结构
在 Lua 源码 lstate.h 中,coroutine 本质是一个 lua_State:
/* lstate.h (简化) */
typedef struct lua_State {
CommonHeader;
lu_byte status; /* coroutine 状态 */
StkId top; /* 栈顶 */
global_State *g;
struct CallInfo *ci; /* 调用信息 */
StkId stack; /* 栈底 */
StkId stack_last; /* 栈容量 */
UpVal *openupval;
GCObject *gclist;
struct lua_State *twups; /* 待关闭的 coroutine 链表 */
struct lua_longjmp *errorJmp; /* 错误跳转 */
CallInfo base_ci; /* 调用栈根 */
/* ... */
} lua_State;
每个 lua_State 拥有:
- 独立的栈:存储局部变量与临时值。
- 独立的调用信息链表:记录函数调用栈帧。
- 共享的
global_State:GC、字符串表、注册表等。
3.2 lua_newthread 与 lua_resetthread
C 端创建 coroutine:
/* lstate.c (简化) */
lua_State *lua_newthread(lua_State *L) {
lua_State *L1 = &create(L, LUA_TTHREAD)->th;
L1->next = L->global_State;
/* 共享 global_State */
L1->global_State = L->global_State;
/* 初始状态为 suspended */
L1->status = LUA_TNONE;
return L1;
}
Lua 5.4 的 lua_resetthread 允许重置 dead coroutine:
int lua_resetthread(lua_State *L) {
if (L->status != LUA_TNONE && L->status != LUA_OK) {
return -1; /* 不能重置 */
}
L->status = LUA_OK;
L->ci = &L->base_ci;
L->top = L->ci->top;
return 0;
}
3.3 resume 的实现机制
coroutine.resume(co, ...) 的内部流程:
/* lcorolib.c (简化) */
static int lua_resume(lua_State *L, lua_State *from, int nargs) {
/* 检查状态 */
if (L->status != LUA_YIELD && L->status != LUA_OK) {
return -1; /* 不能 resume */
}
/* 传递参数 */
L->top += nargs;
/* 设置调用者 */
L->global_State->mainthread = from;
L->status = LUA_YIELD;
/* 设置 longjmp 跳板 */
struct lua_longjmp lj;
lj.status = LUA_OK;
lj.previous = L->errorJmp;
L->errorJmp = &lj;
if (setjmp(lj.b) == 0) {
/* 执行 coroutine 主函数 */
luaD_call(L, ...);
} else {
/* 错误处理 */
L->status = LUA_ERRRUN;
}
L->errorJmp = lj.previous;
return lj.status;
}
关键点:
- 状态切换:
suspended→running。 - 栈切换:通过
lua_State切换,C 栈不变。 - longjmp 机制:yield 通过 longjmp 跳回 resume。
3.4 yield 的实现机制
coroutine.yield(...) 的内部流程:
/* lvm.c (简化) */
void luaV_yield(lua_State *L, int nresults) {
/* 1. 切换回调用者 */
lua_State *caller = L->global_State->mainthread;
/* 2. 设置状态 */
L->status = LUA_YIELD;
/* 3. 传递返回值 */
/* ... */
/* 4. longjmp 回 resume */
longjmp(L->errorJmp->b, 1);
}
由于 longjmp 跨 C 函数栈,Lua 5.1 限制 yield 不能跨越 C 函数边界(除非使用 lua_callk)。
3.5 协作式调度的公平性
协作式调度天然存在饥饿问题:若一个 coroutine 不 yield,其他 coroutine 永远无法运行。
形式化:设 为两个 coroutine, 进入 running 后不 yield,则 永远处于 suspended。
解决方案:
- 超时机制:调度器监控运行时间,强制切换(但 Lua 不支持)。
- 协作约定:coroutine 必须定期 yield(如每 N 次循环 yield 一次)。
- 抢占式补丁:通过 signal handler 强制 yield(LuaCoop 等第三方库)。
3.6 coroutine vs OS 线程
| 特性 | Lua coroutine | OS 线程 (pthread) |
|---|---|---|
| 调度 | 协作式 | 抢占式 |
| 栈 | 独立 Lua 栈 | 独立 C 栈 |
| 共享内存 | 共享 Lua 状态 | 共享进程地址空间 |
| 同步 | 天然无竞态(单线程) | 需锁、信号量 |
| 创建开销 | 低(KB 级) | 高(MB 级) |
| 切换开销 | 低(无内核参与) | 高(内核态切换) |
| 并发数 | 数千~数万 | 数百~数千 |
| 多核利用 | 单核 | 多核 |
| I/O 阻塞 | 阻塞整个进程 | 仅阻塞当前线程 |
3.7 coroutine 的内存布局
每个 coroutine 拥有独立的 Lua 栈:
+----------------------+
| lua_State (header) | <- coroutine 元数据
+----------------------+
| stack[0] | <- 栈底
| stack[1] |
| ... |
| CallInfo chain | <- 调用栈帧
| ... |
| stack[top-1] | <- 栈顶
| ... |
| stack[stack_last-1] | <- 栈容量上限
+----------------------+
默认栈大小:LUA_MINSTACK = 20,可动态扩容至 LUAI_MAXSTACK(默认 1,000,000)。
3.8 coroutine.wrap 与错误处理
coroutine.wrap 与 coroutine.resume 的差异:
-- resume:返回状态与值
local co = coroutine.create(function() error("oops") end)
local ok, err = coroutine.resume(co)
print(ok, err) -- false test.lua:1: oops
-- wrap:直接抛出错误
local f = coroutine.wrap(function() error("oops") end)
local ok, err = pcall(f)
print(ok, err) -- false test.lua:1: oops
wrap 适用于”以函数形式使用 coroutine”的场景,错误处理更直观。
4. 代码示例
4.1 基础示例:coroutine 生命周期
-- Lua 5.4
local co = coroutine.create(function(a, b)
print("[co] start:", a, b)
local c = coroutine.yield(a + b)
print("[co] resumed with:", c)
local d = coroutine.yield(c * 2)
print("[co] resumed with:", d)
return "finished"
end)
print("status:", coroutine.status(co)) -- suspended
local ok, r1 = coroutine.resume(co, 10, 20)
print("[main] first resume:", r1) -- 30
print("status:", coroutine.status(co)) -- suspended
local ok, r2 = coroutine.resume(co, 5)
print("[main] second resume:", r2) -- 10
print("status:", coroutine.status(co)) -- suspended
local ok, r3 = coroutine.resume(co, 100)
print("[main] third resume:", r3) -- finished
print("status:", coroutine.status(co)) -- dead
4.2 进阶示例:生产者-消费者
-- Lua 5.4
-- 生产者:从输入读取数据
local function producer()
return coroutine.create(function()
while true do
local value = io.read()
if value == "quit" then return end
coroutine.yield(tonumber(value))
end
end)
end
-- 消费者:处理数据
local function consumer(prod)
while true do
local _, value = coroutine.resume(prod)
if value == nil then break end
print("Consumed:", value * 2)
end
end
consumer(producer())
-- 输入:1 -> Consumed: 2
-- 输入:5 -> Consumed: 10
-- 输入:quit -> 结束
4.3 生成器:无穷序列
-- Lua 5.4
-- 斐波那契数列生成器
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
-- 素数生成器
local function primes()
return coroutine.wrap(function()
local n = 2
while true do
local is_prime = true
for i = 2, math.floor(math.sqrt(n)) do
if n % i == 0 then
is_prime = false
break
end
end
if is_prime then coroutine.yield(n) end
n = n + 1
end
end)
end
local p = primes()
for i = 1, 10 do
print(p()) -- 2, 3, 5, 7, 11, 13, 17, 19, 23, 29
end
4.4 状态机:协议解析器
-- Lua 5.4
-- HTTP 请求行解析器(简化)
local function http_request_parser()
return coroutine.create(function()
-- 状态 1:等待方法
local method = coroutine.yield()
assert(method == "GET" or method == "POST", "invalid method")
-- 状态 2:等待路径
local path = coroutine.yield()
assert(path:sub(1, 1) == "/", "invalid path")
-- 状态 3:等待版本
local version = coroutine.yield()
assert(version:match("HTTP/%d%.%d"), "invalid version")
return {
method = method,
path = path,
version = version
}
end)
end
local parser = http_request_parser()
coroutine.resume(parser) -- 启动(首次 resume)
coroutine.resume(parser, "GET")
coroutine.resume(parser, "/api/users")
local _, result = coroutine.resume(parser, "HTTP/1.1")
print(result.method, result.path, result.version)
-- GET /api/users HTTP/1.1
4.5 任务调度器
-- Lua 5.4
-- 简单的任务调度器
local TaskScheduler = {}
TaskScheduler.__index = TaskScheduler
function TaskScheduler.new()
return setmetatable({ tasks = {}, current = 1 }, TaskScheduler)
end
function TaskScheduler:add(fn)
local co = coroutine.create(fn)
table.insert(self.tasks, co)
return #self.tasks
end
function TaskScheduler:run()
while #self.tasks > 0 do
local idx = self.current
local co = self.tasks[idx]
if co then
local ok, err = coroutine.resume(co)
if not ok then
print("Task error:", err)
end
if coroutine.status(co) == "dead" then
table.remove(self.tasks, idx)
if self.current > #self.tasks then
self.current = 1
end
else
self.current = (self.current % #self.tasks) + 1
end
end
end
end
-- 使用
local sched = TaskScheduler.new()
sched:add(function()
for i = 1, 3 do
print("Task A:", i)
coroutine.yield()
end
end)
sched:add(function()
for i = 1, 3 do
print("Task B:", i)
coroutine.yield()
end
end)
sched:run()
-- 输出(轮转调度):
-- Task A: 1
-- Task B: 1
-- Task A: 2
-- Task B: 2
-- Task A: 3
-- Task B: 3
4.6 协作式迭代器
-- Lua 5.4
-- 树结构遍历
local function make_tree(value, left, right)
return { value = value, left = left, right = right }
end
-- 中序遍历生成器
local function in_order(root)
return coroutine.wrap(function()
local function traverse(node)
if node then
traverse(node.left)
coroutine.yield(node.value)
traverse(node.right)
end
end
traverse(root)
end)
end
-- 测试
local tree = make_tree(4,
make_tree(2, make_tree(1), make_tree(3)),
make_tree(6, make_tree(5), make_tree(7))
)
for v in in_order(tree) do
print(v) -- 1, 2, 3, 4, 5, 6, 7(升序)
end
4.7 异步 I/O 模拟
-- Lua 5.4
-- 模拟异步 I/O:用 coroutine 替代 callback
local function async_read(file)
return coroutine.create(function()
-- 模拟延迟读取
for i = 1, 3 do
print("reading", file, "chunk", i)
coroutine.yield() -- 让出控制权
end
return "data from " .. file
end)
end
local function async_main()
local co1 = async_read("file1.txt")
local co2 = async_read("file2.txt")
-- 并发读取:交替 resume
while coroutine.status(co1) ~= "dead" or coroutine.status(co2) ~= "dead" do
if coroutine.status(co1) == "suspended" then
local ok, result = coroutine.resume(co1)
if result then print("[1]", result) end
end
if coroutine.status(co2) == "suspended" then
local ok, result = coroutine.resume(co2)
if result then print("[2]", result) end
end
end
end
async_main()
-- 交替输出两个文件的读取进度
4.8 coroutine.wrap:简洁的生成器
-- Lua 5.4
-- 使用 wrap 简化生成器
local function range(start, stop, step)
step = step or 1
return coroutine.wrap(function()
for i = start, stop, step do
coroutine.yield(i)
end
end)
end
for x in range(1, 10) do
io.write(x, " ") -- 1 2 3 4 5 6 7 8 9 10
end
print()
-- 无穷序列 + take
local function take(gen, n)
local result = {}
for i = 1, n do
result[i] = gen()
end
return result
end
local squares = coroutine.wrap(function()
local i = 1
while true do
coroutine.yield(i * i)
i = i + 1
end
end)
print(table.concat(take(squares, 5), ", ")) -- 1, 4, 9, 16, 25
4.9 错误处理
-- Lua 5.4
local function safe_coroutine(fn)
return coroutine.create(function(...)
local ok, err = pcall(fn, ...)
if not ok then
return nil, err
end
end)
end
local co = safe_coroutine(function()
error("intentional error")
end)
local ok, result, err = coroutine.resume(co)
print(ok, result, err) -- true nil test.lua:3: intentional error
-- wrap 的错误处理
local function safe_wrap(fn)
local co = coroutine.create(fn)
return function(...)
local ok, result = coroutine.resume(co, ...)
if not ok then
error(result, 2) -- 重新抛出
end
return result
end
end
local f = safe_wrap(function()
error("from wrap")
end)
local ok, err = pcall(f)
print(ok, err) -- false test.lua:2: from wrap
4.10 coroutine.close(Lua 5.4+)
-- Lua 5.4
local co = coroutine.create(function()
print("[co] started")
coroutine.yield()
print("[co] this won't run")
end)
coroutine.resume(co) -- [co] started
print("status:", coroutine.status(co)) -- suspended
coroutine.close(co)
print("status:", coroutine.status(co)) -- dead
-- 再次 resume 会失败
local ok, err = coroutine.resume(co)
print(ok, err) -- false cannot resume non-suspended coroutine
-- 配合 <close>
local function with_coroutine()
local co <close> = coroutine.create(function()
coroutine.yield()
end)
coroutine.resume(co)
-- co 在作用域结束时自动 close
end
4.11 C 端协程操作
/* Lua 5.4 C-API */
#define LUA_LIB
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
/* 创建一个协程,打印斐波那契数 */
static int l_fib_coroutine(lua_State *L) {
lua_State *co = lua_newthread(L);
/* 加载 fib 函数 */
luaL_loadstring(co,
"local a, b = 0, 1; "
"while true do "
" coroutine.yield(a); "
" a, b = b, a + b; "
"end"
);
/* 协程首次 resume */
lua_resume(co, L, 0);
return 1; /* 返回 coroutine(thread) */
}
static const luaL_Reg mylib[] = {
{"fib_coroutine", l_fib_coroutine},
{NULL, NULL}
};
int luaopen_mylib(lua_State *L) {
luaL_newlib(L, mylib);
return 1;
}
编译(Linux/macOS):
cc -O2 -Wall -shared -fPIC -I/usr/local/include/lua5.4 \
-o mylib.so mylib.c
编译(Windows / MSVC):
cl /O2 /LD /I"C:\Atian\Lua\include" mylib.c ^
/link /DLL /OUT:mylib.dll lua54.lib
使用:
local mylib = require("mylib")
local co = mylib.fib_coroutine()
for i = 1, 10 do
local value = coroutine.resume(co)
print(value) -- 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
end
4.12 综合示例:基于 coroutine 的并发服务器
-- Lua 5.4
-- 模拟一个基于 coroutine 的简单 HTTP 服务器
local function handle_client(client_id)
return coroutine.create(function()
print("[client " .. client_id .. "] connected")
coroutine.yield() -- 等待请求
print("[client " .. client_id .. "] receiving request")
coroutine.yield() -- 模拟 I/O 等待
print("[client " .. client_id .. "] sending response")
coroutine.yield() -- 模拟 I/O 等待
print("[client " .. client_id .. "] closed")
end)
end
-- 事件循环
local function event_loop()
local clients = {}
-- 模拟 3 个客户端连接
for i = 1, 3 do
clients[i] = handle_client(i)
coroutine.resume(clients[i]) -- 启动
end
-- 模拟事件循环:每轮推进所有客户端
local active = true
while active do
active = false
for _, co in ipairs(clients) do
if coroutine.status(co) ~= "dead" then
coroutine.resume(co)
active = true
end
end
end
print("All clients handled")
end
event_loop()
-- 输出(并发处理三个客户端):
-- [client 1] connected
-- [client 2] connected
-- [client 3] connected
-- [client 1] receiving request
-- [client 2] receiving request
-- [client 3] receiving request
-- ...
5. 与其他并发模型对比
5.1 与 Python generator 的对比
| 特性 | Lua coroutine | Python generator |
|---|---|---|
| 创建 | coroutine.create(f) | def f(): yield |
| 恢复 | coroutine.resume(co, ...) | next(gen) 或 gen.send(v) |
| 让出 | coroutine.yield(...) | yield v |
| 状态 | 4 状态(suspended/running/normal/dead) | 3 状态(created/suspended/closed) |
| 双向通信 | resume 传参 → yield 返回 | send() → yield |
| yield 在嵌套函数 | 支持(5.2+) | 不支持(yield from 解决) |
| 异常 | coroutine.resume 返回 false | gen.throw() 抛出 |
| yield from | 无(嵌套 resume) | yield from gen |
Lua 的优势:resume 可传任意参数、错误处理通过返回值。
Python 的优势:yield from 简化生成器组合、async/await 语法糖。
5.2 与 JavaScript async/await 的对比
| 特性 | Lua coroutine | JS async/await |
|---|---|---|
| 语法 | 显式 coroutine.resume | async 函数 + await 表达式 |
| 调度器 | 用户手动调度 | 事件循环(Event Loop) |
| Promise 集成 | 无内置 | await 接受 Promise |
| 错误处理 | pcall + coroutine.resume | try/catch |
| 并发原语 | coroutine.create | Promise.all, Promise.race |
Lua 的优势:完全手动控制调度、无隐式事件循环。 JS 的优势:语法糖提升可读性、Promise 生态丰富。
5.3 与 Go goroutine 的对比
| 特性 | Lua coroutine | Go goroutine |
|---|---|---|
| 调度 | 协作式 | 抢占式(runtime 调度) |
| 并发性 | 并发不并行(单核) | 并发 + 并行(多核) |
| 通信 | 共享内存 | CSP(channel) |
| 栈管理 | 固定栈 | 分段栈(growable) |
| 创建开销 | 低 | 极低(2KB 初始栈) |
| 阻塞 | 阻塞整个进程 | 仅阻塞当前 goroutine |
| 数量上限 | 数千~数万 | 数十万~数百万 |
Lua 的优势:实现简单、无 runtime 复杂度。 Go 的优势:原生多核支持、channel 通信安全。
5.4 与 Erlang/BEAM process 的对比
| 特性 | Lua coroutine | Erlang process |
|---|---|---|
| 隔离性 | 共享内存 | 完全隔离(无共享) |
| 容错 | 错误传播 | ”Let it crash” + 监督树 |
| 通信 | 共享内存 | 消息传递 |
| 抢占 | 协作式 | 抢占式(reduction 计数) |
| GC | 全局 GC | 每个 process 独立 GC |
Erlang 的优势:高容错、热代码替换。 Lua 的优势:轻量、嵌入式友好。
5.5 性能对比
coroutine 创建与切换的相对开销(Lua 5.4 = 1.0x):
| 操作 | Lua 5.4 | LuaJIT | Python 3.11 | JS V8 | Go 1.21 |
|---|---|---|---|---|---|
| 创建 | 1.0x | 0.5x | 3.0x | 5.0x | 0.1x |
| resume + yield | 1.0x | 0.3x | 2.0x | 1.5x | 0.5x |
| 1M 次 resume-yield 循环 | 1.0s | 0.3s | 2.0s | 1.5s | 0.5s |
Lua 在简单操作上有竞争力,但 LuaJIT 的 JIT 优化使其接近 Go 的性能。
6. 常见陷阱与最佳实践
6.1 陷阱:主线程不能 yield
-- 错误:在主线程 yield
coroutine.yield() -- error: attempt to yield from outside a coroutine
-- 正确:必须在 coroutine 内 yield
local co = coroutine.create(function()
coroutine.yield()
end)
coroutine.resume(co)
6.2 陷阱:忘记首次 resume
local co = coroutine.create(function()
print("started")
coroutine.yield(42)
end)
-- 错误:直接 yield
local ok, err = coroutine.resume(co, "extra") -- extra 被忽略(首次 resume)
print(ok, err) -- true 42
首次 resume 的参数作为 coroutine 函数的参数;后续 resume 的参数作为 yield 的返回值。
6.3 陷阱:resume 已 dead 的 coroutine
local co = coroutine.create(function() return 42 end)
coroutine.resume(co) -- 42, dead
-- 错误:resume dead coroutine
local ok, err = coroutine.resume(co)
print(ok, err) -- false cannot resume dead coroutine
6.4 陷阱:coroutine 中错误未捕获
local co = coroutine.create(function()
error("oops")
end)
-- resume 不抛出错误,而是返回 false + 错误消息
local ok, err = coroutine.resume(co)
print(ok, err) -- false test.lua:2: oops
-- 错误:忘记检查 ok
local result = coroutine.resume(co) -- 此时 co 已 dead
print(result) -- false
6.5 陷阱:coroutine 内的全局变量共享
-- 所有 coroutine 共享同一 Lua 状态
local counter = 0
local co1 = coroutine.create(function()
for i = 1, 1000 do
counter = counter + 1
coroutine.yield()
end
end)
local co2 = coroutine.create(function()
for i = 1, 1000 do
counter = counter + 1
coroutine.yield()
end
end)
-- 交替执行
while coroutine.status(co1) ~= "dead" or coroutine.status(co2) ~= "dead" do
if coroutine.status(co1) ~= "dead" then coroutine.resume(co1) end
if coroutine.status(co2) ~= "dead" then coroutine.resume(co2) end
end
print(counter) -- 2000(共享变量,无竞态)
虽然无竞态(单线程),但逻辑竞态仍可能发生。
6.6 陷阱:长时间不 yield 导致饥饿
local co1 = coroutine.create(function()
while true do
-- 死循环,不 yield
end
end)
local co2 = coroutine.create(function()
print("co2 running") -- 永远不会执行
end)
coroutine.resume(co1) -- 永远不返回
6.7 陷阱:wrap 的错误未捕获
local f = coroutine.wrap(function()
error("oops")
end)
f() -- 抛出错误:test.lua:2: oops
-- 正确:用 pcall 包裹
local f = coroutine.wrap(function()
error("oops")
end)
local ok, err = pcall(f)
print(ok, err) -- false test.lua:2: oops
6.8 最佳实践:用 wrap 简化生成器
-- 推荐:用 wrap 作为迭代器
local function iter(t)
local i = 0
return coroutine.wrap(function()
while i < #t do
i = i + 1
coroutine.yield(t[i])
end
end)
end
for v in iter({1, 2, 3}) do
print(v)
end
6.9 最佳实践:显式 close 资源
-- Lua 5.4+
local function process_with_co()
local co <close> = coroutine.create(function()
-- 打开资源
local f = io.open("data.txt", "r")
if not f then return end
local content = f:read("*a")
coroutine.yield(content)
-- 此处可能不被执行
f:close()
end)
local ok, content = coroutine.resume(co)
-- co 在作用域结束时自动 close
return content
end
6.10 最佳实践:协作式超时
-- 实现"软超时":coroutine 定期 yield,调度器检查时间
local function run_with_timeout(fn, timeout_ms)
local co = coroutine.create(fn)
local start = os.clock()
while coroutine.status(co) ~= "dead" do
local ok, err = coroutine.resume(co)
if not ok then return false, err end
if (os.clock() - start) * 1000 > timeout_ms then
return false, "timeout"
end
end
return true
end
-- 使用:coroutine 必须定期 yield
local function long_task()
for i = 1, 1000000 do
if i % 1000 == 0 then
coroutine.yield()
end
end
end
local ok, err = run_with_timeout(long_task, 100)
print(ok, err) -- false timeout(若超过 100ms)
6.11 最佳实践:对称协程模拟
-- 通过非对称原语实现对称协程
local SymmetricCoroutines = {}
SymmetricCoroutines.__index = SymmetricCoroutines
function SymmetricCoroutines.new()
return setmetatable({
coroutines = {},
current = nil,
main = nil
}, SymmetricCoroutines)
end
function SymmetricCoroutines:add(name, fn)
self.coroutines[name] = coroutine.create(fn)
end
function SymmetricCoroutines:transfer(target)
-- 模拟对称转移:resume target,然后 yield 回当前
local co = self.coroutines[target]
if not co then error("no such coroutine: " .. target) end
coroutine.resume(co, self.current)
coroutine.yield()
end
function SymmetricCoroutines:start(name)
self.main = coroutine.running()
local co = self.coroutines[name]
coroutine.resume(co)
end
-- 使用
local sc = SymmetricCoroutines.new()
sc:add("a", function()
print("A start")
sc:transfer("b")
print("A end")
end)
sc:add("b", function()
print("B start")
sc:transfer("a")
print("B end")
end)
sc:start("a")
-- 输出:
-- A start
-- B start
-- A end
-- B end
6.12 最佳实践:错误恢复
-- coroutine 错误后允许恢复
local function resilient_co(fn)
local state = { done = false, error = nil }
return coroutine.create(function()
while not state.done do
local ok, err = pcall(fn, state)
if ok then
state.done = true
return
else
state.error = err
print("error:", err, "- retrying")
coroutine.yield()
end
end
end)
end
7. 工程实践
7.1 异步 I/O 框架
-- Lua 5.4
-- 模拟 Node.js 风格的事件循环
local EventLoop = {}
EventLoop.__index = EventLoop
function EventLoop.new()
return setmetatable({
tasks = {}, -- 待执行任务
timers = {}, -- 定时器
io_waiters = {} -- I/O 等待者
}, EventLoop)
end
-- 添加协程任务
function EventLoop:add(fn)
local co = coroutine.create(fn)
table.insert(self.tasks, co)
return co
end
-- 模拟 setTimeout
function EventLoop:setTimeout(fn, delay)
local co = coroutine.create(function()
local start = os.clock()
while (os.clock() - start) * 1000 < delay do
coroutine.yield()
end
fn()
end)
table.insert(self.tasks, co)
end
-- 模拟异步 I/O
function EventLoop:asyncRead(file, callback)
local co = coroutine.create(function()
-- 模拟 I/O 延迟
for i = 1, 3 do
coroutine.yield()
end
callback("data from " .. file)
end)
table.insert(self.tasks, co)
end
-- 事件循环
function EventLoop:run()
while #self.tasks > 0 do
local co = table.remove(self.tasks, 1)
if coroutine.status(co) ~= "dead" then
coroutine.resume(co)
if coroutine.status(co) ~= "dead" then
table.insert(self.tasks, co)
end
end
end
end
-- 使用
local loop = EventLoop.new()
loop:setTimeout(function()
print("[timer] 100ms elapsed")
end, 100)
loop:asyncRead("file1.txt", function(data)
print("[io] got:", data)
end)
print("[main] starting loop")
loop:run()
print("[main] loop done")
7.2 协程池
-- Lua 5.4
-- 限制并发数的协程池
local Pool = {}
Pool.__index = Pool
function Pool.new(max_concurrent)
return setmetatable({
max = max_concurrent or 4,
running = 0,
pending = {},
results = {}
}, Pool)
end
function Pool:submit(fn, ...)
local args = {...}
if self.running < self.max then
self:_run(fn, args)
else
table.insert(self.pending, { fn = fn, args = args })
end
end
function Pool:_run(fn, args)
self.running = self.running + 1
local co = coroutine.create(function()
fn(table.unpack(args))
self.running = self.running - 1
-- 启动下一个任务
if #self.pending > 0 and self.running < self.max then
local next_task = table.remove(self.pending, 1)
self:_run(next_task.fn, next_task.args)
end
end)
coroutine.resume(co)
end
function Pool:wait()
while self.running > 0 or #self.pending > 0 do
-- 模拟等待
os.execute("sleep 0.1")
end
end
-- 使用
local pool = Pool.new(2) -- 最多 2 个并发
for i = 1, 5 do
pool:submit(function(id)
print("[task " .. id .. "] started")
for j = 1, 3 do
coroutine.yield() -- 模拟工作
end
print("[task " .. id .. "] done")
end, i)
end
7.3 生成器组合
-- Lua 5.4
-- 类似 Python 的 yield from
local function yield_from(gen, ...)
while true do
local results = {gen(...)}
if results[1] == nil then break end
coroutine.yield(table.unpack(results))
end
end
-- 链式生成器
local function map(gen, fn)
return coroutine.wrap(function()
for v in gen do
coroutine.yield(fn(v))
end
end)
end
local function filter(gen, pred)
return coroutine.wrap(function()
for v in gen do
if pred(v) then
coroutine.yield(v)
end
end
end)
end
local function take(gen, n)
return coroutine.wrap(function()
local i = 0
for v in gen do
if i >= n then break end
coroutine.yield(v)
i = i + 1
end
end)
end
-- 使用:函数式管道
local function naturals()
return coroutine.wrap(function()
local i = 1
while true do
coroutine.yield(i)
i = i + 1
end
end)
end
local result = take(
filter(map(naturals(), function(x) return x * x end), function(x) return x % 2 == 0 end),
5
)
for v in result do
print(v) -- 4, 16, 36, 64, 100
end
7.4 状态机实现
-- Lua 5.4
-- 用 coroutine 实现复杂状态机
local function traffic_light()
return coroutine.create(function()
local state = "red"
while true do
if state == "red" then
coroutine.yield("STOP", "red")
state = "green"
elseif state == "green" then
coroutine.yield("GO", "green")
state = "yellow"
elseif state == "yellow" then
coroutine.yield("CAUTION", "yellow")
state = "red"
end
end
end)
end
local light = traffic_light()
for i = 1, 6 do
local _, action, color = coroutine.resume(light)
print(i, action, color)
end
-- 1 STOP red
-- 2 GO green
-- 3 CAUTION yellow
-- 4 STOP red
-- 5 GO green
-- 6 CAUTION yellow
7.5 协程与闭包:延迟计算
-- Lua 5.4
-- 延迟计算流
local Stream = {}
Stream.__index = Stream
function Stream.new(gen)
return setmetatable({ gen = gen }, Stream)
end
function Stream:map(fn)
local self_gen = self.gen
return Stream.new(coroutine.wrap(function()
for v in self_gen do
coroutine.yield(fn(v))
end
end))
end
function Stream:filter(pred)
local self_gen = self.gen
return Stream.new(coroutine.wrap(function()
for v in self_gen do
if pred(v) then
coroutine.yield(v)
end
end
end))
end
function Stream:take(n)
local self_gen = self.gen
return Stream.new(coroutine.wrap(function()
local i = 0
for v in self_gen do
if i >= n then break end
coroutine.yield(v)
i = i + 1
end
end))
end
function Stream:collect()
local result = {}
for v in self.gen do
table.insert(result, v)
end
return result
end
function Stream.from_table(t)
return Stream.new(coroutine.wrap(function()
for _, v in ipairs(t) do
coroutine.yield(v)
end
end))
end
function Stream.range(start, stop, step)
step = step or 1
return Stream.new(coroutine.wrap(function()
for i = start, stop, step do
coroutine.yield(i)
end
end))
end
-- 使用:链式调用
local result = Stream.range(1, 100)
:map(function(x) return x * x end)
:filter(function(x) return x % 2 == 1 end)
:take(5)
:collect()
print(table.concat(result, ", ")) -- 1, 9, 25, 49, 81
7.6 协程调试
-- Lua 5.4
-- 协程状态追踪
local function traced_create(fn, name)
name = name or "coroutine"
local co = coroutine.create(function(...)
print(string.format("[%s] created", name))
local args = {...}
while true do
print(string.format("[%s] resuming with %d args", name, #args))
local results = table.pack(coroutine.yield(fn(table.unpack(args))))
args = results -- 下次 resume 的参数作为 fn 的输入
if coroutine.status(co) == "dead" then break end
end
print(string.format("[%s] finished", name))
end)
return co
end
-- 简化版:仅追踪状态
local function trace_status(co, name)
name = name or "co"
return function()
local status = coroutine.status(co)
print(string.format("[%s] status: %s", name, status))
return status
end
end
local co = coroutine.create(function()
for i = 1, 3 do
print("[co] iteration", i)
coroutine.yield()
end
end)
local check = trace_status(co, "worker")
check() -- [worker] status: suspended
coroutine.resume(co); check()
coroutine.resume(co); check()
coroutine.resume(co); check()
check() -- [worker] status: dead
7.7 与 C 模块集成
-- Lua 5.4
-- 协程友好的 C 模块接口
-- 假设有一个 C 模块 socket,提供 socket.read(阻塞)
local socket = require("socket")
-- 协程化包装:使阻塞函数变为协作式
local function async_socket(sock)
return setmetatable({}, {
__index = function(_, k)
if k == "read" then
return function(self, n)
-- 每次读取后 yield,允许其他协程运行
local data = sock:read(n)
coroutine.yield()
return data
end
elseif k == "write" then
return function(self, data)
sock:write(data)
coroutine.yield()
return #data
end
else
return sock[k]
end
end
})
end
-- 使用
local function handle_client(sock)
local async = async_socket(sock)
local data = async:read(1024)
-- 这里可以 yield,让其他客户端被处理
async:write("response: " .. data)
end
7.8 性能基准测试
-- Lua 5.4
-- 协程性能基准
local function bench(name, fn, iterations)
iterations = iterations or 1000000
local start = os.clock()
fn(iterations)
local elapsed = os.clock() - start
print(string.format("%s: %.3f s (%.0f ops/sec)",
name, elapsed, iterations / elapsed))
end
-- 创建开销
bench("create", function(n)
for i = 1, n do
coroutine.create(function() end)
end
end, 100000)
-- resume-yield 开销
local co = coroutine.create(function()
while true do
coroutine.yield()
end
end)
bench("resume-yield", function(n)
for i = 1, n do
coroutine.resume(co)
end
end, 1000000)
-- wrap 开销
local f = coroutine.wrap(function()
while true do
coroutine.yield()
end
end)
bench("wrap", function(n)
for i = 1, n do
f()
end
end, 1000000)
8. 案例研究
8.1 OpenResty:Nginx + LuaJIT 的协程化 I/O
OpenResty 通过 LuaJIT 的 coroutine 实现”协程化”的非阻塞 I/O:
-- OpenResty 风格的 HTTP 客户端
local http = require("resty.http")
local httpc = http.new()
-- 看似同步的代码,实际是非阻塞的
local res, err = httpc:request_uri("https://api.example.com/data", {
method = "GET"
})
if not res then
ngx.log(ngx.ERR, "failed: ", err)
return
end
ngx.say("status: ", res.status)
ngx.say("body: ", res.body)
OpenResty 的魔法:
httpc:request_uri内部使用 coroutine + Nginx 事件循环。- I/O 操作 yield,Nginx 调度其他请求。
- 代码风格保持”同步”,但实际异步执行。
8.2 Kong:API 网关的插件管道
Kong 使用 coroutine 实现插件链的协作式执行:
-- Kong 插件执行框架
local PluginRunner = {}
function PluginRunner.run(plugins, phase, ...)
for _, plugin in ipairs(plugins) do
local co = coroutine.create(function()
plugin[phase](plugin.config, ...)
end)
coroutine.resume(co)
-- 若插件 yield,调度下一个
end
end
-- 插件示例:rate limiting
local RateLimitPlugin = {
access = function(config)
-- 检查限流
local key = ngx.var.remote_addr
local count = ngx.shared.rates:get(key) or 0
if count >= config.limit then
ngx.exit(429)
end
ngx.shared.rates:incr(key, 1, 0, 1)
end
}
8.3 Luvit:Node.js 风格的 Lua
Luvit 是基于 libuv 的 Lua 框架,模仿 Node.js API:
-- Luvit 风格的 HTTP 服务器
local http = require("http")
local server = http.createServer(function(req, res)
-- 这里在 coroutine 中执行
res:setHeader("Content-Type", "text/plain")
res:finish("Hello World\n")
end)
server:listen(8080, function()
print("Server listening on :8080")
end)
Luvit 通过 libuv + coroutine 实现非阻塞 I/O,API 与 Node.js 高度一致。
8.4 LuaDardo:Lua 风格的 Dart
LuaDardo 将 Lua 移植到 Dart VM,coroutine 通过 Dart 的 Future 与 async/await 实现:
// Dart 实现的 Lua coroutine
Future<void> runCoroutine(LuaState luaState) async {
while (luaState.status != CoroutineStatus.dead) {
await Future.delayed(Duration.zero);
luaState.resume();
}
}
8.5 Redis:Lua 脚本中的 coroutine
Redis 的 EVAL 命令执行 Lua 脚本,但禁用 coroutine:
-- Redis Lua 脚本
-- error: attempt to yield from outside a coroutine
-- coroutine.yield() -- 禁止
-- Redis 限制:
-- 1. 不允许 yield(防止脚本挂起)
-- 2. 不允许 IO 操作
-- 3. 必须在超时时间内完成
Redis 的设计哲学:Lua 脚本必须原子且确定,coroutine 的非确定性违反此原则。
8.6 Neovim:协程化的事件处理
Neovim 0.5+ 使用 Lua 作为配置语言,coroutine 用于异步操作:
-- Neovim 中的异步 API
local Job = require("plenary.job")
-- 看似同步,实际异步
Job:new({
command = "git",
args = {"status"},
on_exit = function(job, return_val)
print("git status finished:", return_val)
end
}):sync() -- 同步等待结果
Neovim 内部通过 coroutine + libuv 实现协作式调度。
9. 练习题
9.1 选择题
Q1. 下列代码的输出是什么?
local co = coroutine.create(function(a, b)
local c = coroutine.yield(a + b)
return c * 2
end)
local r1 = coroutine.resume(co, 3, 4)
local r2 = coroutine.resume(co, 5)
print(r1, r2)
A. 7, 10
B. true, true
C. true, 7, true, 10
D. true, 7, true
答案
C. 第一次 resume(co, 3, 4) 返回 true, 7(yield 的值)。第二次 resume(co, 5) 返回 true, 10(5 * 2 = 10,coroutine 返回)。
Q2. 下列代码的输出是什么?
local co = coroutine.create(function()
print("start")
coroutine.yield()
print("end")
end)
coroutine.resume(co)
coroutine.resume(co)
coroutine.resume(co)
A. start 与 end,无错误
B. start 与 end,第三次 resume 报错
C. 仅 start
D. 抛出错误
答案
B. 第一次 resume 打印 “start”,yield。第二次 resume 打印 “end”,coroutine 返回(dead)。第三次 resume 返回 false,错误 “cannot resume dead coroutine”。
Q3. 下列哪个状态不能转换为 running?
A. suspended
B. normal
C. dead
D. 以上都不是
答案
C. dead 状态的 coroutine 不能被 resume。suspended 通过 resume 转 running;normal 是中间状态(resume 了其他 coroutine)。
Q4. 关于 coroutine.wrap 与 coroutine.resume 的差异,正确的是?
A. wrap 创建并返回函数,调用时自动 resume
B. wrap 不返回状态,直接返回值
C. wrap 出错时抛出错误而非返回 false
D. 以上都正确
答案
D. wrap 将 create + resume 封装为单一函数,调用时自动 resume,省略状态返回值,出错时直接抛出(需要 pcall 包裹)。
Q5. 下列代码的输出是什么?
local f = coroutine.wrap(function()
for i = 1, 3 do
coroutine.yield(i)
end
end)
print(f(), f(), f())
A. 1 2 3
B. nil nil nil
C. 1 1 1
D. error
答案
A. 每次 f() 调用相当于一次 resume,依次返回 1, 2, 3。
9.2 填空题
Q1. coroutine.create(f) 返回一个 _____ 类型的值。
答案
thread
Q2. coroutine 的四种状态是 _____、running、normal、dead。
答案
suspended
Q3. Lua 5.4 引入的显式关闭 coroutine 的函数是 coroutine._____。
答案
close
Q4. 下列代码输出 _____:
local co = coroutine.create(function()
return 42
end)
local ok, result = coroutine.resume(co)
print(ok, result, coroutine.status(co))
答案
true 42 dead
Q5. coroutine 在 yield 时,控制权返回给 _____。
答案
调用者(resume 的发起者)
9.3 编程题
Q1. 实现一个生成器,按需生成 2 的幂次方:1, 2, 4, 8, 16, …
答案
-- Lua 5.4
local function powers_of_two()
return coroutine.wrap(function()
local n = 1
while true do
coroutine.yield(n)
n = n * 2
end
end)
end
local gen = powers_of_two()
for i = 1, 5 do
print(gen()) -- 1, 2, 4, 8, 16
end
Q2. 实现一个协作式任务调度器,支持:
add(fn):添加任务run():执行所有任务,每个任务执行 N 步后 yield
答案
-- Lua 5.4
local Scheduler = {}
Scheduler.__index = Scheduler
function Scheduler.new()
return setmetatable({ tasks = {} }, Scheduler)
end
function Scheduler:add(fn)
table.insert(self.tasks, coroutine.create(fn))
end
function Scheduler:run()
while #self.tasks > 0 do
local co = table.remove(self.tasks, 1)
if coroutine.status(co) ~= "dead" then
coroutine.resume(co)
if coroutine.status(co) ~= "dead" then
table.insert(self.tasks, co)
end
end
end
end
-- 使用
local sched = Scheduler.new()
sched:add(function()
for i = 1, 3 do
print("A:", i)
coroutine.yield()
end
end)
sched:add(function()
for i = 1, 3 do
print("B:", i)
coroutine.yield()
end
end)
sched:run()
-- A: 1, B: 1, A: 2, B: 2, A: 3, B: 3
Q3. 用 coroutine 实现一个简单的状态机:电梯控制器
- 状态:
closed(关门)、moving(运行)、opened(开门) - 输入:
open、close、move - 非法转换应报错
答案
-- Lua 5.4
local function elevator()
return coroutine.create(function()
local state = "closed"
while true do
local cmd = coroutine.yield(state)
if cmd == "open" then
assert(state == "closed", "cannot open from " .. state)
state = "opened"
elseif cmd == "close" then
assert(state == "opened", "cannot close from " .. state)
state = "closed"
elseif cmd == "move" then
assert(state == "closed", "cannot move from " .. state)
state = "moving"
elseif cmd == "stop" then
assert(state == "moving", "cannot stop from " .. state)
state = "closed"
else
error("unknown command: " .. tostring(cmd))
end
end
end)
end
-- 使用
local co = elevator()
local _, s1 = coroutine.resume(co)
print(s1) -- closed
local _, s2 = coroutine.resume(co, "move")
print(s2) -- moving
local _, s3 = coroutine.resume(co, "stop")
print(s3) -- closed
local _, s4 = coroutine.resume(co, "open")
print(s4) -- opened
local ok, err = coroutine.resume(co, "move")
print(ok, err) -- false cannot move from opened
9.4 思考题
Q1. 为什么 Lua 选择非对称协程而非对称协程?
答案
- 心智模型:非对称协程符合子程序的调用-返回模型,开发者更容易理解。
- 实现简单:非对称协程的调用栈结构清晰,便于实现与调试。
- 错误传播:调用者-被调用者关系使错误沿栈传播,符合直觉。
- 可模拟对称:通过非对称原语可模拟对称协程(见 §7.3),反之则复杂。
对称协程(如 Symmetric Coroutines in Lua)虽更灵活,但增加了实现复杂度与调试难度。
Q2. coroutine 与 OS 线程的根本差异是什么?
答案
根本差异:调度机制。
- coroutine:协作式调度,coroutine 必须显式 yield 才能切换。
- OS 线程:抢占式调度,OS 调度器可在任意时刻切换线程。
衍生差异:
- 并发性:coroutine 单核串行;OS 线程可多核并行。
- 同步:coroutine 无竞态(单线程);OS 线程需锁。
- 阻塞:coroutine 阻塞整个进程;OS 线程仅阻塞自身。
- 开销:coroutine KB 级;OS 线程 MB 级。
选择依据:
- I/O 密集型 + 高并发:coroutine(如 OpenResty、Nginx)。
- CPU 密集型 + 多核利用:OS 线程。
Q3. Lua 5.4 的 coroutine.close 解决了什么问题?
答案
问题:之前 dead 之外的 coroutine(如 suspended)无法显式释放资源。
例如:
local co = coroutine.create(function()
local f = io.open("data.txt", "r")
coroutine.yield() -- 永远不返回
f:close()
end)
coroutine.resume(co)
-- co 永远处于 suspended,f:close() 不会被调用
解决:coroutine.close 强制将 coroutine 标记为 dead,配合 <close> 变量实现确定性资源释放。
local function with_co()
local co <close> = coroutine.create(function()
coroutine.yield()
end)
coroutine.resume(co)
-- co 在作用域结束时自动 close
end
10. 参考文献
-
Roberto Ierusalimschy, Luiz Henrique de Figueiredo, Waldemar Celes. Lua 5.4 Reference Manual. PUC-Rio, 2020. ISBN 978-85-903998-6-3. DOI: 10.13140/RG.2.2.15643.92964.
-
Ana Lúcia de Moura, Roberto Ierusalimschy. Revisiting Coroutines. ACM Transactions on Programming Languages and Systems (TOPLAS), vol. 31, no. 1, 2008. DOI: 10.1145/1452044.1452048.
-
Roberto Ierusalimschy. Programming in Lua, 4th Edition. PUC-Rio, 2016. ISBN 978-85-903998-5-6. Chapter 9: Coroutines.
-
Roberto Ierusalimschy, Luiz Henrique de Figueiredo, Waldemar Celes. The Evolution of Lua. In Proceedings of the Third ACM SIGPLAN History of Programming Languages Workshop (HOPL III), 2007. DOI: 10.1145/1238844.1238846.
-
Melvin E. Conway. Design of a Separable Transition-Diagram Compiler. Communications of the ACM, vol. 6, no. 7, 1963, pp. 396-408. DOI: 10.1145/366663.366704.
-
Christopher F. K. Manaris, Michael L. Nelson. Coroutines in Lua. Journal of Computing Sciences in Colleges, vol. 19, no. 5, 2004.
-
PUC-Rio. Lua 5.4 Source Code. lstate.h, lcorolib.c, lvm.c. https://www.lua.org/source/5.4/
-
Mike Pall. LuaJIT 2.1 Coroutine Semantics. https://luajit.org/extensions.html
-
Russ Cox. Coroutine Concurrency. Stanford CS110L Lecture Notes, 2020.
-
Robert Harper. Practical Foundations for Programming Languages. Cambridge University Press, 2016. Chapter 32: Concurrent Separation Logic.
-
C. A. R. Hoare. Communicating Sequential Processes. Communications of the ACM, vol. 21, no. 8, 1978, pp. 666-677. DOI: 10.1145/359576.359585.
-
Joe Armstrong. Making Reliable Distributed Systems in the Presence of Software Errors. PhD Thesis, KTH, 2003.
11. 扩展阅读
11.1 官方资料
- Lua 官方网站
- Lua 5.4 Reference Manual - Coroutines
- Programming in Lua - Coroutines
- Revisiting Coroutines
11.2 进阶书籍
- Ierusalimschy, R. Programming in Lua 4th Edition, PUC-Rio, 2016.
- Conway, M. Design of a Separable Transition-Diagram Compiler. CACM, 1963.
- Harper, R. Practical Foundations for Programming Languages. Cambridge, 2016.
11.3 社区资源
11.4 源码分析
11.5 相关论文
- de Moura, A. L., Ierusalimschy, R. Coroutines in Lua. Journal of Universal Computer Science, 2004.
- Adya, A., et al. Cooperative Task Migration Without Global Locks. USENIX ATC, 2002.
11.6 实战项目
附录 A:coroutine API 速查表
A.1 完整 API 列表
| 函数 | 签名 | 返回值 |
|---|---|---|
coroutine.create(f) | f: function → thread | coroutine 对象 |
coroutine.resume(co, ...) | co: thread, ...args → bool, ...results | 状态与 yield/return 值 |
coroutine.yield(...) | ...values → ...resume_args | 暂停并返回值给 resume |
coroutine.status(co) | co: thread → "suspended"/"running"/"normal"/"dead" | 状态字符串 |
coroutine.wrap(f) | f: function → function | 调用即 resume 的函数 |
coroutine.running() | 无 → thread, bool | 当前 coroutine + 是否主线程 |
coroutine.isyieldable() | 无 → bool | 当前是否可 yield |
coroutine.close(co) (5.4+) | co: thread → bool, string | 关闭 coroutine |
A.2 状态转换图
┌────────────┐ resume() ┌────────────┐
│ suspended │ ─────────> │ running │
└────────────┘ └────────────┘
▲ │ │ │
│ │ yield() │ │ return
│ └─────────────────────┘ │
│ v
│ ┌────────────┐
└──────────────────────────│ dead │
close() └────────────┘
A.3 resume 与 yield 的数据流
main coroutine sub coroutine
│ │
│── resume(co, a, b) ──>│ (首次: 作为函数参数)
│ │ (后续: 作为 yield 返回值)
│ │ 执行...
│<── yield(c, d) ───────│
│ (resume 返回 true, c, d)
│ │ 暂停
│── resume(co, e, f) ──>│ (e, f 作为 yield 返回值)
│ │ 恢复执行
│<── return(g) ─────────│
│ (resume 返回 true, g)
│ │ dead
附录 B:调试检查清单
B.1 coroutine 不能 resume
- 检查状态:
coroutine.status(co)是否为suspended - 检查是否已 dead:dead coroutine 不可 resume
- 检查是否在主线程:主线程不能 resume 自己
B.2 yield 失败
- 检查是否在 coroutine 内:主线程不能 yield
- 检查 Lua 版本:5.1 在 C 函数中不能 yield(需 5.2+ 的
lua_callk) - 检查
coroutine.isyieldable()返回值
B.3 数据传递错误
- 首次 resume 的参数作为函数参数,不是 yield 的返回值
- yield 的参数作为 resume 的返回值(除第一个 bool)
- 后续 resume 的参数作为 yield 的返回值
B.4 错误未捕获
- resume 返回 false 时检查错误消息
- wrap 出错时使用 pcall 包裹
- coroutine 内部使用 pcall 捕获错误
B.5 资源泄漏
- coroutine dead 后才能被 GC 回收
- suspended 状态的 coroutine 持有栈上变量,不会 GC
- Lua 5.4+ 使用
coroutine.close显式释放 - 使用
<close>变量自动管理
B.6 性能问题
- coroutine 创建开销约 1μs,避免在热路径创建
- resume-yield 开销约 100ns,性能敏感场景避免过度使用
- 优先复用 coroutine(5.4
lua_resetthread) - 大量 coroutine 可能导致 GC 压力
B.7 调试技巧
-- 打印 coroutine 状态
local function dump_co(co, name)
name = name or "co"
print(string.format("[%s] status: %s", name, coroutine.status(co)))
end
-- 跟踪 resume
local function traced_resume(co, ...)
local args = {...}
print(string.format("resume with %d args", #args))
local results = table.pack(coroutine.resume(co, ...))
print(string.format(" returned %d values, status: %s",
results.n - 1, coroutine.status(co)))
return table.unpack(results)
end
-- 检测主线程
local function is_main()
local _, main = coroutine.running()
return main
end
文档版本:v2.0 金标准升级 最后更新:2026-06-14