lua-源码带读-08-协程与多任务
Lua 5.4 源码带读 第8篇:协程与多任务
本篇目标
深入理解 Lua 协程(coroutine)的实现:lua_State 作为协程的容器、yield/resume 机制、非抢占式调度、协作式多任务模式。
前置知识
- 了解协程与线程的区别
- 了解yield/resume概念
- 阅读过第1-4篇
1. 协程基础
1.1 什么是协程
协程是协作式多任务的基本单位。与线程不同:
- 协程可以主动让出执行权(yield)
- 协程不能被其他协程强制抢占
- 一个协程在任何时刻只在一个线程中运行
1.2 协程API
// lua.h
lua_State *lua_newthread(lua_State *L); // 创建新协程
int lua_resume(lua_State *L, lua_State *from, int nargs, int *nresults);
int lua_yieldk(lua_State *L, int nresults, lua_KContext ctx, lua_KFunction k);
1.3 Lua标准库API
-- 协程库
local co = coroutine.create(function()
coroutine.yield(100)
return "done"
end)
coroutine.resume(co) -- 恢复执行,返回 true, 100
coroutine.resume(co) -- 恢复执行,返回 true, "done"
coroutine.status(co) -- "dead"
coroutine.yield(...) -- 让出执行权
2. 协程的内部表示
2.1 每个协程是一个lua_State
// lstate.c
lua_State *lua_newthread(lua_State *L) {
lua_State *L1 = luaE_newthread(L);
setthvalue(L, L->top, L1);
api_incr_top(L);
return L1;
}
每个协程都有独立的:
- 值栈(value stack)
- 调用信息链(CallInfo)
- 错误恢复点(errorJmp)
- 线程状态(status)
2.2 协程状态
// lua.h
#define LUA_OK 0 // 正常运行
#define LUA_YIELD 1 // 已挂起
#define LUA_ERRRUN 2 // 运行时错误
#define LUA_ERRSYNTAX 3 // 语法错误
#define LUA_ERRMEM 4 // 内存错误
#define LUA_ERRERR 5 // 错误处理函数错误
3. yield 机制
3.1 lua_yieldk
// ldo.c
int lua_yieldk(lua_State *L, int nresults, lua_KContext ctx, lua_KFunction k) {
// 检查是否在C调用中
if (L->ci->callstatus & CIST_YIELD)
// 已经在挂起状态,不能再yield
luaG_runerror(L, "attempt to yield across a C-call boundary");
// 设置挂起状态
L->status = LUA_YIELD;
// 保存返回值到栈上
StkId func = L->ci->func;
// 移动返回值到正确位置
// 如果有continuation函数,保存上下文
if (k != NULL) {
L->ci->u.c.k = k;
L->ci->u.c.ctx = ctx;
L->ci->callstatus = CIST_YIELD | CIST_YPCALL;
}
return LUA_YIELD;
}
3.2 resume 机制
// ldo.c
int lua_resume(lua_State *L, lua_State *from, int nargs, int *nresults) {
// 检查状态
if (L->status != LUA_YIELD && L->status != LUA_OK)
return LUA_ERRRUN;
// 恢复执行
StkId firstResult = luaD_rawrunprotected(L, resume, &nargs);
if (firstResult == NULL) {
// yield,返回挂起状态
*nresults = 0;
return LUA_YIELD;
} else {
// 完成或出错
// 设置返回值
return LUA_OK;
}
}
3.3 yield 的栈处理
coroutine.resume(co)
→ 从调用者栈复制参数到协程栈
→ 恢复执行
→ 协程执行 yield
→ 返回值留在协程栈顶
→ 将返回值复制回调用者栈
→ resume 返回 true, 返回值...
4. continuation 机制
4.1 什么是continuation
Lua 5.1 只能在C函数调用边界之前yield。Lua 5.4 通过 continuation 机制支持跨C调用边界的yield。
-- 5.1: 不能在C函数中yield
-- 5.4: 可以在C函数中yield
local co = coroutine.create(function()
local ok, err = pcall(function()
-- pcall是C函数,但可以在其中yield
coroutine.yield(100)
end)
end)
4.2 continuation函数
// lua.h
typedef lua_CFunction lua_KFunction; // continuation函数
continuation函数在resume时被调用,处理yield后的恢复。
5. 协作式多任务模式
5.1 生产者-消费者
local producer = coroutine.create(function()
while true do
local item = produce_item()
coroutine.yield(item)
end
end)
local consumer = coroutine.create(function()
while true do
local ok, item = coroutine.resume(producer)
if not ok then break end
consume_item(item)
end
end)
5.2 协程调度器
local function scheduler(threads)
while #threads > 0 do
for i, co in ipairs(threads) do
if coroutine.status(co) ~= "dead" then
local ok, val = coroutine.resume(co)
if not ok then
table.remove(threads, i)
end
else
table.remove(threads, i)
end
end
end
end
6. 非抢占式 vs 抢占式
| 特性 | 协作式(Lua协程) | 抢占式(OS线程) |
|---|---|---|
| 切换时机 | 显式yield | 由调度器强制切换 |
| 数据竞争 | 不可能(手动yield) | 可能(需要锁) |
| 上下文切换开销 | 低(只保存栈) | 高(保存寄存器等) |
| 编程复杂度 | 低(无锁) | 高(需要同步原语) |
| 适用场景 | 协作式多任务 | 真正的并行计算 |
7. 本篇小结
| 概念 | 要点 |
|---|---|
| 协程 = lua_State | 每个协程有独立的栈和CallInfo |
| yield | 主动让出执行权,保留栈状态 |
| resume | 恢复协程执行 |
| continuation | 跨C调用边界的yield(5.4新增) |
| 非抢占式 | 协程之间不会发生数据竞争 |
| 状态管理 | LUA_OK/LUA_YIELD/LUA_ERR* |
思考题
- Lua 协程和操作系统线程在上下文切换开销上有什么区别?
- continuation机制解决了什么问题?
- 为什么Lua选择非抢占式而非抢占式协程?
- 如何用Lua协程实现一个简单的状态机?
coroutine.wrap和coroutine.create有什么区别?
思考题解答
1. 协程和OS线程的上下文切换开销区别
| 特性 | Lua协程 | OS线程 |
|---|---|---|
| 切换开销 | 极低(~100ns) | 较高(~1-10μs) |
| 保存内容 | 只保存lua_State(栈+CallInfo) | 保存所有寄存器+TLB+浮点状态 |
| 内存开销 | 每个协程约1-8KB | 每个线程约1-8MB(栈空间) |
| 创建开销 | 内存分配+初始化 | 系统调用+内核数据结构 |
| 切换方式 | 显式yield | 由调度器中断 |
开销对比:
- Lua协程切换:只需移动指针(保存/恢复lua_State),约100纳秒
- OS线程切换:需要保存/恢复所有CPU寄存器、刷新TLB、可能的内核态切换,约1-10微秒
- 差距约10-100倍
内存对比:
- Lua协程:每个协程一个lua_State,初始栈约1KB,最大可配置
- OS线程:默认栈空间1-8MB,内核还要维护线程控制块
- 可以创建数千个Lua协程,但OS线程通常限制在数百个
2. continuation机制解决了什么问题?
问题:Lua 5.1中,C函数不能在被调用时yield。如果C函数内部调用了Lua代码,而Lua代码yield了,会导致"跨C调用边界的yield"错误。
-- Lua 5.1中会报错
local co = coroutine.create(function()
pcall(function()
coroutine.yield(100) -- pcall是C函数,不能yield
end)
end)
解决方案:5.4引入continuation机制,允许C函数注册一个continuation函数。当C函数内部yield时,resume时不是恢复C函数,而是调用continuation函数继续执行。
// C函数返回continuation函数
static int my_cfunc(lua_State *L) {
// 执行部分工作
return lua_yieldk(L, 1, ctx, my_continuation);
}
// continuation函数:在resume时被调用
static int my_continuation(lua_State *L, int status, lua_KContext ctx) {
// 继续未完成的工作
}
效果:C函数可以"透明地"支持yield,对Lua代码来说就像普通函数一样。
3. 为什么选择非抢占式协程?
- 简单性:不需要锁、原子操作、内存屏障等同步原语
- 性能:无锁设计,切换开销极低
- 确定性:协程只在显式yield点切换,行为可预测
- 无数据竞争:同一个协程在任何时刻只在一个线程中运行
- 与Lua的嵌入式定位一致:Lua主要用于单线程环境,不需要真正的并行
代价:
- 不能利用多核CPU(需要配合多进程或外部线程库)
- 长时间运行的协程会阻塞其他协程
- 需要程序员手动yield(协作点)
适用场景:
- 协作式多任务(游戏AI、网络服务器)
- 迭代器和生成器
- 状态机
- 异步I/O回调
4. 如何用协程实现状态机?
local function create_states()
-- 状态:每个状态是一个协程
local idle = coroutine.create(function()
while true do
print("状态: IDLE")
local event = coroutine.yield("waiting")
if event == "start" then
return "running", event -- 切换到running状态
end
end
end)
local running = coroutine.create(function()
while true do
print("状态: RUNNING")
local event = coroutine.yield("processing")
if event == "stop" then
return "idle", event -- 切换到idle状态
end
end
end)
return { idle = idle, running = running }
end
-- 状态机调度器
local states = create_states()
local current = "idle"
local function dispatch(event)
local co = states[current]
local ok, next_state, data = coroutine.resume(co, event)
if next_state then
current = next_state
end
end
dispatch("start") -- IDLE -> RUNNING
dispatch("stop") -- RUNNING -> IDLE
原理:每个状态是一个协程,状态转换通过 return 实现,事件通过 resume 传递。
5. coroutine.wrap 和 coroutine.create 的区别
| 特性 | coroutine.create | coroutine.wrap |
|---|---|---|
| 返回值 | 协程对象(userdata) | 迭代器函数 |
| 错误处理 | 返回 ok, err |
直接抛出错误 |
| 使用方式 | coroutine.resume(co, ...) |
co(...) |
| 底层实现 | 创建lua_State | 创建lua_State + 包装函数 |
-- create + resume
local co = coroutine.create(function() return 42 end)
local ok, val = coroutine.resume(co)
print(ok, val) -- true 42
-- wrap
local co = coroutine.wrap(function() return 42 end)
local val = co()
print(val) -- 42
-- 如果协程报错,wrap会直接抛出错误
选择建议:
- 需要错误处理 →
create + resume - 简单迭代 →
wrap(更简洁) - 需要检查协程状态 →
create(可以使用coroutine.status)

浙公网安备 33010602011771号