📄 Lua 协程:从 API 到底层原理再到 Skynet 架构的完整学习路径
一句话总结:本文以"渐进追问"的方式完整走通了协程的学习路径------从 API 使用到底层原理(栈帧/PC/lua_State),再到 Skynet 中 Actor+协程的架构融合,最终揭示协程是"用户态操作系统"中的线程调度单元。
流程图
#mermaid-svg-fiwpynmpXlAFGlf8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fiwpynmpXlAFGlf8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fiwpynmpXlAFGlf8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fiwpynmpXlAFGlf8 .error-icon{fill:#a44141;}#mermaid-svg-fiwpynmpXlAFGlf8 .error-text{fill:#ddd;stroke:#ddd;}#mermaid-svg-fiwpynmpXlAFGlf8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fiwpynmpXlAFGlf8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fiwpynmpXlAFGlf8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fiwpynmpXlAFGlf8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fiwpynmpXlAFGlf8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fiwpynmpXlAFGlf8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fiwpynmpXlAFGlf8 .marker{fill:lightgrey;stroke:lightgrey;}#mermaid-svg-fiwpynmpXlAFGlf8 .marker.cross{stroke:lightgrey;}#mermaid-svg-fiwpynmpXlAFGlf8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fiwpynmpXlAFGlf8 p{margin:0;}#mermaid-svg-fiwpynmpXlAFGlf8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#ccc;}#mermaid-svg-fiwpynmpXlAFGlf8 .cluster-label text{fill:#F9FFFE;}#mermaid-svg-fiwpynmpXlAFGlf8 .cluster-label span{color:#F9FFFE;}#mermaid-svg-fiwpynmpXlAFGlf8 .cluster-label span p{background-color:transparent;}#mermaid-svg-fiwpynmpXlAFGlf8 .label text,#mermaid-svg-fiwpynmpXlAFGlf8 span{fill:#ccc;color:#ccc;}#mermaid-svg-fiwpynmpXlAFGlf8 .node rect,#mermaid-svg-fiwpynmpXlAFGlf8 .node circle,#mermaid-svg-fiwpynmpXlAFGlf8 .node ellipse,#mermaid-svg-fiwpynmpXlAFGlf8 .node polygon,#mermaid-svg-fiwpynmpXlAFGlf8 .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#mermaid-svg-fiwpynmpXlAFGlf8 .rough-node .label text,#mermaid-svg-fiwpynmpXlAFGlf8 .node .label text,#mermaid-svg-fiwpynmpXlAFGlf8 .image-shape .label,#mermaid-svg-fiwpynmpXlAFGlf8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-fiwpynmpXlAFGlf8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fiwpynmpXlAFGlf8 .rough-node .label,#mermaid-svg-fiwpynmpXlAFGlf8 .node .label,#mermaid-svg-fiwpynmpXlAFGlf8 .image-shape .label,#mermaid-svg-fiwpynmpXlAFGlf8 .icon-shape .label{text-align:center;}#mermaid-svg-fiwpynmpXlAFGlf8 .node.clickable{cursor:pointer;}#mermaid-svg-fiwpynmpXlAFGlf8 .root .anchor path{fill:lightgrey!important;stroke-width:0;stroke:lightgrey;}#mermaid-svg-fiwpynmpXlAFGlf8 .arrowheadPath{fill:lightgrey;}#mermaid-svg-fiwpynmpXlAFGlf8 .edgePath .path{stroke:lightgrey;stroke-width:2.0px;}#mermaid-svg-fiwpynmpXlAFGlf8 .flowchart-link{stroke:lightgrey;fill:none;}#mermaid-svg-fiwpynmpXlAFGlf8 .edgeLabel{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-svg-fiwpynmpXlAFGlf8 .edgeLabel p{background-color:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-fiwpynmpXlAFGlf8 .edgeLabel rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-fiwpynmpXlAFGlf8 .labelBkg{background-color:rgba(87.75, 87.75, 87.75, 0.5);}#mermaid-svg-fiwpynmpXlAFGlf8 .cluster rect{fill:hsl(180, 1.5873015873%, 28.3529411765%);stroke:rgba(255, 255, 255, 0.25);stroke-width:1px;}#mermaid-svg-fiwpynmpXlAFGlf8 .cluster text{fill:#F9FFFE;}#mermaid-svg-fiwpynmpXlAFGlf8 .cluster span{color:#F9FFFE;}#mermaid-svg-fiwpynmpXlAFGlf8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(20, 1.5873015873%, 12.3529411765%);border:1px solid rgba(255, 255, 255, 0.25);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fiwpynmpXlAFGlf8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#mermaid-svg-fiwpynmpXlAFGlf8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-fiwpynmpXlAFGlf8 .icon-shape,#mermaid-svg-fiwpynmpXlAFGlf8 .image-shape{background-color:hsl(0, 0%, 34.4117647059%);text-align:center;}#mermaid-svg-fiwpynmpXlAFGlf8 .icon-shape p,#mermaid-svg-fiwpynmpXlAFGlf8 .image-shape p{background-color:hsl(0, 0%, 34.4117647059%);padding:2px;}#mermaid-svg-fiwpynmpXlAFGlf8 .icon-shape .label rect,#mermaid-svg-fiwpynmpXlAFGlf8 .image-shape .label rect{opacity:0.5;background-color:hsl(0, 0%, 34.4117647059%);fill:hsl(0, 0%, 34.4117647059%);}#mermaid-svg-fiwpynmpXlAFGlf8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fiwpynmpXlAFGlf8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fiwpynmpXlAFGlf8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-fiwpynmpXlAFGlf8 .start>*{fill:#1a5276!important;color:#fff!important;stroke:#2980b9!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .start span{fill:#1a5276!important;color:#fff!important;stroke:#2980b9!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .start tspan{fill:#fff!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .core>*{fill:#4a235a!important;color:#fff!important;stroke:#8e44ad!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .core span{fill:#4a235a!important;color:#fff!important;stroke:#8e44ad!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .core tspan{fill:#fff!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .decision>*{fill:#7d6608!important;color:#fff!important;stroke:#f39c12!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .decision span{fill:#7d6608!important;color:#fff!important;stroke:#f39c12!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .decision tspan{fill:#fff!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .solution>*{fill:#145a32!important;color:#fff!important;stroke:#27ae60!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .solution span{fill:#145a32!important;color:#fff!important;stroke:#27ae60!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .solution tspan{fill:#fff!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .question>*{fill:#78281f!important;color:#fff!important;stroke:#e74c3c!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .question span{fill:#78281f!important;color:#fff!important;stroke:#e74c3c!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .question tspan{fill:#fff!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .philosophy>*{fill:#7b241c!important;color:#fff!important;stroke:#c0392b!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .philosophy span{fill:#7b241c!important;color:#fff!important;stroke:#c0392b!important;}#mermaid-svg-fiwpynmpXlAFGlf8 .philosophy tspan{fill:#fff!important;} 起点:学习Lua协程API
追问:为什么能暂停?
原理:lua_State + 栈帧 + PC
判断:要不要读Lua源码?
结论:知道独立栈就够了
应用:yield有什么意义?
三层场景:回调消除/惰性迭代/同步调用
架构融合:协程+Actor = Skynet
终极洞察:Skynet是用户态操作系统
学习策略:工程人员的编译原理最小集
内容梳理
一、协程 API:四个核心函数
协程就是一个能主动暂停、然后从暂停点继续运行的函数。普通函数一路跑到 return 结束,而协程跑到 yield 暂停、等 resume 继续。
lua
-- create:创建协程(不执行)
local co = coroutine.create(function()
print("A: 协程开始了")
coroutine.yield("我暂停了,这个值传回给调用者")
print("B: 协程被恢复了")
return "done"
end)
-- 第一次 resume:启动协程,跑到 yield 为止
local ok, yield_value = coroutine.resume(co)
-- 输出: A: 协程开始了
-- 返回: true, "我暂停了,这个值传回给调用者"
-- 第二次 resume:从 yield 处继续,直到 return
local ok, result = coroutine.resume(co)
-- 输出: B: 协程被恢复了
-- 返回: true, "done"
关键数据流规则:resume 的参数 = 上一次 yield 的返回值;yield 的参数 = 这一次 resume 的返回值。 这是一个双向管道------
lua
local co = coroutine.create(function()
local x = coroutine.yield("给我一个数") -- 传出 "给我一个数",等 resume 传入 x
local y = coroutine.yield("再给我一个数") -- 传出 "再给我一个数",等 resume 传入 y
return x + y
end)
coroutine.resume(co) -- → 协程跑到第一个 yield,收到 "给我一个数"
coroutine.resume(co, 10) -- → x=10,收到 "再给我一个数"
coroutine.resume(co, 20) -- → y=20,收到 30
主程序 协程
resume(参数) →
coroutine.yield() 收到参数
← yield返回值
resume(参数) →
coroutine.yield() 收到参数
← yield返回值
resume(参数) →
return 值
← return值
二、为什么能暂停:从 CPU 寄存器到 lua_State
协程能暂停,因为它的"执行现场"(栈、指令指针、局部变量)保存在独立的数据结构 lua_State 里,不依赖物理 CPU 寄存器。
普通函数不能暂停的原因:CPU 只有一套 PC(Program Counter - 程序计数器)、SP(Stack Pointer - 栈指针)、BP(Base Pointer - 基址指针),函数调用时这些寄存器被当前函数独占。
Lua 5.2+ 的设计:每个协程有自己独立的 lua_State:
主线程 lua_State 协程的 lua_State
┌─────────────────┐ ┌─────────────────┐
│ 栈 (Stack) │ │ 栈 (Stack) │
│ ci (调用链) │ │ ci (调用链) │
│ savedpc (指令指针)│ │ savedpc (指令指针)│
│ top (栈顶) │ │ top (栈顶) │
│ base (栈底) │ │ base (栈底) │
│ 全局表 _G ◄──────┼───────┤ 全局表 _G (共享!) │
└─────────────────┘ └─────────────────┘
- 每个协程有自己的栈:局部变量、函数调用链都在自己的栈上
- 共享全局表
_G(这就是为什么 Actor 模型要每个 Actor 独立的lua_State------避免共享全局变量) - "切换"就是改一个指针:告诉 Lua 虚拟机"现在用这个
lua_State"
yield 和 resume 的伪代码流程:
coroutine.yield(...) 时:
1. 把 yield 的参数压到自己的栈上
2. 把当前 lua_State 的状态设为 LUA_YIELD
3. 把控制权交还给调用者 lua_State → 调用者从 resume() 返回
4. 协程的 lua_State 保持不动 → 栈、指令指针、局部变量全部保留
coroutine.resume(co, ...) 时:
1. 把 resume 的参数压到 co 的栈上
2. 把当前 lua_State 切换到 co
3. co 从上次 yield 的下一行继续执行 → yield(...) 返回 resume 传进来的参数
三、C 语言最小实现:用 swapcontext 感受协程本质
下面 80 行 C 代码用 POSIX ucontext 实现了协程核心------手动切换栈和指令指针。协程切换 = 保存当前上下文 → 恢复目标上下文。
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ucontext.h>
#define STACK_SIZE (1024 * 64)
typedef struct {
ucontext_t ctx; // 执行上下文:寄存器 + PC + SP
char stack[STACK_SIZE]; // 这个协程自己的栈
int finished;
} coroutine_t;
coroutine_t main_ctx;
coroutine_t co;
void co_func(void) {
for (int i = 1; i <= 3; i++) {
printf(" 协程: 运行到第 %d 步,准备 yield\n", i);
swapcontext(&co.ctx, &main_ctx); // yield: 切回主程序
}
printf(" 协程: 结束了\n");
co.finished = 1;
swapcontext(&co.ctx, &main_ctx);
}
int main(void) {
getcontext(&co.ctx);
co.ctx.uc_stack.ss_sp = co.stack;
co.ctx.uc_stack.ss_size = STACK_SIZE;
co.ctx.uc_link = NULL;
makecontext(&co.ctx, co_func, 0);
printf("主程序: 创建了协程\n");
while (!co.finished) {
printf("主程序: resume 协程 →\n");
swapcontext(&main_ctx, &co.ctx); // 切到协程
printf("主程序: ← 协程 yield 了,主程序继续\n\n");
}
printf("主程序: 协程已结束\n");
return 0;
}
输出:
主程序: 创建了协程
主程序: resume 协程 →
协程: 运行到第 1 步,准备 yield
主程序: ← 协程 yield 了,主程序继续
主程序: resume 协程 →
协程: 运行到第 2 步,准备 yield
主程序: ← 协程 yield 了,主程序继续
主程序: resume 协程 →
协程: 运行到第 3 步,准备 yield
主程序: ← 协程 yield 了,主程序继续
主程序: resume 协程 →
协程: 结束了
主程序: 协程已结束
swapcontext 做的事情:旧上下文保存当前 SP/BP/PC → 新上下文加载新 SP/BP/PC。协程有自己的栈内存块,切换就是把 CPU 寄存器存到旧协程的结构体、从新协程的结构体加载。全程在用户态,不经过内核,极快(几十 ns)。
四、三层原理总结
┌──────────────────────────────────────────────────┐
│ Lua 层面: coroutine.yield() / coroutine.resume() │
│ 语义: "暂停" / "继续" │
├──────────────────────────────────────────────────┤
│ C 运行时: 每个协程 = 一个独立的 lua_State │
│ lua_State 里: 自己的栈 + CallInfo链 + savedpc │
│ yield = 标记 YIELD,切回调用者 │
│ resume = 切换全局 L → co 的 lua_State │
├──────────────────────────────────────────────────┤
│ OS 层面: ucontext / setjmp-longjmp / 手写汇编 │
│ 保存/恢复 CPU 寄存器 + 栈指针 + PC │
│ 全程用户态,不经过内核 │
└──────────────────────────────────────────────────┘
五、要不要读 Lua 源码?------学习深度的判断
我面临的核心困惑:理解了 lua_State 是协程的基础,是否应该读 Lua 解释器源码?我没有系统学过编译原理。
结论:不要读。 Lua 源码里有词法分析器、递归下降解析器、字节码生成器、寄存器式虚拟机、GC(Garbage Collection - 垃圾回收)实现,每一项都是一门课。我现在需要的是知道"协程有自己的 lua_State,暂停不丢局部变量"------这个深度就够了。
工程人员需要的"编译原理最小集":
| 优先级 | 内容 | 用到的地方 |
|---|---|---|
| ✅ 高 | 栈帧结构、调用约定、PC、符号表 | 理解协程切换、内核模块、strace 输出 |
| ❌ 低 | 词法分析、语法分析、AST、IR、寄存器分配 | 几乎用不到 |
学习深度七层判断------我在第3-4层(够用了),再深入是时间投资回报递减。
以后若需补底层,顺序为:CS:APP 第三章+第八章(栈帧与异常控制流)→ 《Lua 设计与实现》→ 带着问题读 Lua 源码(如只追 lua_yield 一条调用链),每步隔半年。
六、yield 有什么意义?------三个工程场景
来自我嵌入式背景的困惑:"程序不就是要运行完成吗?暂停干什么?"
核心认知转变: 服务器程序不是"跑完",是"一直在跑,等很多件事"。嵌入式用轮询(while(1) + sleep),服务器不能轮询(几千连接,CPU 白烧),也不能阻塞(一个卡住全挂)。协程解决的就是"等的时候不占 CPU,等到了从断点继续"。
场景一:消除回调地狱
lua
-- 不用协程:逻辑散落在匿名回调里,无法用 if/else 写连续流程
function handle_request_1(db, cache, req)
db.query("SELECT ...", function(rows)
cache.set("user", rows, function()
respond(200, rows)
end)
end)
end
-- 用协程:同步写法,整个函数逻辑在一处
function handle_request_1(db, cache, req)
local rows = db:query("SELECT ...") -- yield
cache:set("user", rows) -- yield
respond(200, rows)
end
场景二:惰性迭代器------用 coroutine.wrap 按需生成数据
lua
function log_iterator(filename)
return coroutine.wrap(function()
local file = io.open(filename)
for line in file:lines() do
coroutine.yield(line) -- 你要一条我给一条
end
file:close()
end)
end
for line in log_iterator("huge.log") do
if line:match("ERROR") then
print(line) -- 找到了就停,剩下行根本不生成
break
end
end
场景三:Skynet 中的 skynet.call
lua
local function monitor()
while true do
local temp = skynet.call("sensor_service", "lua", "read_temp")
-- ↑ yield,等 I2C 返回(可能 50ms)
-- 这 50ms 里 Service 内其他协程继续跑
if temp > 80 then
skynet.send("fan_service", "lua", "full_speed")
end
skynet.sleep(500) -- 又 yield
end
end
对比表:
| 场景 | 不 yield 的结果 | yield 的效果 |
|---|---|---|
| 等数据库 | 线程阻塞 10ms,期间不能做任何事 | 协程挂起,其他协程照常运行 |
| 大文件遍历 | 100万行全读进内存 | 每次只有一行在内存 |
| 等 I2C 传感器 | 整个 BMC 固件卡住 | 协程挂起,其他守护进程继续 |
| 无限序列 | 不可能,会死循环 | 每次 yield 返回一个值 |
七、协程 + Actor = Skynet:迷你实现
lua
-- ============================================
-- 迷你版 Skynet:用纯 Lua 模拟 Actor + 协程
-- ============================================
local actors = {}
local function new_actor(name, handler)
actors[name] = { name = name, handler = handler, inbox = {} }
end
local function send(to_name, from_name, msg)
table.insert(actors[to_name].inbox, {from = from_name, msg = msg})
end
-- 同步调用:用协程实现!
local function call(to_name, from_name, msg)
table.insert(actors[to_name].inbox, {
from = from_name, msg = msg,
reply_to = coroutine.running(), -- 当前协程用于回复
})
return coroutine.yield() -- 挂起自己,等回复
end
local function reply(reply_to, result)
coroutine.resume(reply_to, result)
end
local function process_actor(actor)
local msg = table.remove(actor.inbox, 1)
if msg then
coroutine.resume(coroutine.create(function()
actor.handler(msg.from, msg.msg, msg.reply_to)
end))
return true
end
return false
end
local function run_loop(rounds)
for _ = 1, rounds do
for _, actor in pairs(actors) do
process_actor(actor)
end
end
end
-- 测试
new_actor("calculator", function(from, msg, reply_to)
if msg.op == "add" then
reply(reply_to, msg.a + msg.b)
end
end)
new_actor("client", function(from, msg, reply_to)
local result = call("calculator", "client", {op = "add", a = 3, b = 4})
print(string.format("3 + 4 = %d", result))
end)
run_loop(10)
-- 输出:
-- [系统] 创建 Actor: calculator
-- [系统] 创建 Actor: client
-- [client] 发起同步调用: 3 + 4 ...
-- [calculator] 收到 client 的请求: 3 + 4
-- [client] 收到回复: 3 + 4 = 7
skynet.call 的本质就是 coroutine.yield,skynet.ret 的本质就是 coroutine.resume。
八、协程 vs Service vs Worker:三级层次
这是我的核心困惑之一。三者的关系:
┌──────────────────┐
│ skynet 进程 │
│ │
│ ┌──────────────┐│ ← Service A (一个 Actor)
│ │ lua_State_A ││ 独立 VM,自己的全局变量
│ │ 协程1 协程2 ││ 自己的消息队列
│ │ 协程3 ││ 崩了不影响 B
│ │ [msg queue] ││
│ └──────────────┘│
│ ┌──────────────┐│ ← Service B
│ │ lua_State_B ││
│ │ 协程1 ││
│ │ [msg queue] ││
│ └──────────────┘│
│ │
│ Worker线程×8 ←─┼── 从各 msg queue 取消息
│ │ 投给对应 lua_State
│ │ 在里面 fork 协程执行
└──────────────────┘
三级对应表:
| 层级 | 实体 | 被谁调度 | 隔离性 | 数量 |
|---|---|---|---|---|
| OS 层 | Worker 线程 | 内核调度器 | 内存共享(同一进程) | 8 个 |
| 框架层 | Service (Actor) | Worker 抢消息 | lua_State 隔离 |
几百~几千 |
| 语言层 | 协程 | lua_resume / lua_yield |
同一 lua_State 内共享 _G |
每 Service 可 fork 很多 |
关键规则:一个 lua_State 同一时刻只被一个 Worker 持有。 同一 Service 里同时只有一个协程在跑,消息天然串行------这就是 Service 内部不用加锁的原因。
Worker 线程的核心逻辑(简化):
c
void* worker_thread(void* arg) {
while (1) {
message_t* msg = global_queue_pop(); // 从全局队列抢消息
lua_State* L = get_service_lua_state(msg->target_service);
push_message_to_L(L, msg);
lua_resume(L, NULL, 0); // fork/resume 协程
// 协程 yield 或 return → 回去抢下一条
}
}
一次 skynet.call 的完整过程:
- Service A 的协程 C1 把消息放进 B 的消息队列 →
coroutine.yield()→ C1 挂起 - 某 Worker 拿到 B 的
lua_State→ fork 协程 C2 → C2 处理请求 →skynet.ret(result) skynet.ret找到 A 中挂起的 C1 → 把 result push 到 A 的lua_State- Worker 拿到 A 的
lua_State→coroutine.resume(C1, result) - C1 从 yield 下一行醒来 → 继续跑
九、Skynet 是用户态操作系统
我悟到了这个类比:
┌─────────────────────────────────────────────────────────┐
│ Linux 操作系统 │ Skynet(用户态) │
├──────────────────────────────┼──────────────────────────┤
│ 内核 │ skynet 进程 │
│ 物理 CPU │ 8 个 Worker 线程 │
│ 进程(独立地址空间) │ Service(独立 lua_State) │
│ 线程(共享地址空间) │ 协程(共享 lua_State) │
│ 内核调度器 │ Worker 抢消息队列 │
│ 进程间通信(管道/共享内存) │ 消息队列(skynet.send) │
│ 进程崩溃不影响其他进程 │ Service 崩溃不影响其他 │
│ 内核态 / 用户态 │ C 层 / Lua 层 │
└─────────────────────────────────────────────────────────┘
操作系统对进程做的事,Skynet 对 Service 重做了一遍。 区别:Linux 隔物理内存页,Skynet 隔 Lua 虚拟机;Linux 在内核态,Skynet 在用户态。不用起 1000 个进程(fork 几十 ms),起 1000 个 Service(一个 lua_State 几 KB,瞬间),里面再起协程(一个栈缓冲区,微秒级创建)。
Actor 模型的三条规矩(私有状态、消息通信、自主决策)正是现代操作系统的进程模型。Hewitt 原话:"Actor 应该像独立的计算机一样,通过消息通信。"
┌─────────────┐
│ Linux 内核 │
│ 1 个进程 │
│ 8 个线程 │
└──────┬──────┘
│ ← 内核负责
═══════════════════════╪═════════════════
│ ← Skynet 负责
┌────────────┼────────────┐
│ skynet 进程 │
│ Worker×8 ─ 消息队列 │
│ S1(lua_State) │
│ S2(lua_State) │
│ 每个S内部: 协程×N │
└─────────────────────────┘
十、lua_State 的隔离:Lua 自带 vs Skynet 附加
lua_State 的隔离是 Lua C API 自带的,不是 Skynet 发明的。
c
lua_State *L1 = luaL_newstate(); // VM1:独立栈、独立 _G、独立寄存器
lua_State *L2 = luaL_newstate(); // VM2:完全隔离
lua_pushstring(L1, "hello from L1");
lua_setglobal(L1, "msg");
lua_pushstring(L2, "hello from L2");
lua_setglobal(L2, "msg");
// 各读各的,L1 崩了 L2 还活着
| Lua 自带的能力 | Skynet 加的东西 |
|---|---|
luaL_newstate() 创建隔离 VM |
✅ 消息队列:Service A → 消息 → Service B |
独立栈、独立 _G、独立寄存器 |
✅ Worker 线程池:8 线程公平调度所有 Service |
| 一个崩了不影响另一个 | ✅ 协程管理:消息到了自动 fork 协程 |
| ❌ 没有 L1 给 L2 发消息的机制 | ✅ 定时器、服务发现、热更新、网络、日志、集群 |
| ❌ 没有调度 |
纯 Lua 代码不能创建多个 lua_State ------lua_State 是 C API 的概念。纯 Lua 里"隔离"只有两种弱近似:协程(有独立栈但共享 _G)和 _ENV 环境表(纸皮墙,debug 库可逃逸)。隔离是 Lua 自带的,协作是 Skynet 加的。
总结与展望
总结
- 协程的本质是"执行现场可保存/恢复的函数",底层靠独立
lua_State存储栈帧与指令指针 - 学习深度的判断原则:够用为止------知道独立栈 + PC 切换即可,不读 Lua 源码,编译原理只取"最小内核"
- yield 解决了"等时不占 CPU"的问题,三类典型场景:消除回调地狱、惰性迭代、跨 Service 同步调用
- Skynet = Lua 的
lua_State隔离 + 消息队列通信 + Worker 线程调度 = 用户态操作系统 - 协程→Service→Worker 三级层次:协程是 Service 内并发单位,Service 是 Actor(独立 VM),Worker 是共享算力池
展望/趋势
- 协程是异步编程的终局范式 :从回调→Promise→async/await,所有主流语言最终都走向了"同步写法 + 异步执行",协程是该范式的本质抽象。理解了 Lua 协程再学 Python
asyncio、Gogoroutine、Rustasync,只需对比语义差异 - Actor+协程的融合架构是分布式系统的标准答案:Erlang/OTP、Akka、Orleans、Skynet 无不采用此模式。微服务、Kubernetes、Serverless 在更粗粒度上重复同一逻辑------消息驱动 + 无共享 + 故障隔离
- 用户态调度(M:N 模型)是高性能并发的关键:Skynet 的 Worker 线程复用模型等价于 Go 的 GMP 调度器------把协程调度留在用户态,不走内核上下文切换。未来设计高性能系统,这是一项核心判断力
- 建议后续深入方向 :① 在 Skynet 实战中刻意体会"消息驱动"和"共享内存驱动"两种思维的差异;② 对比 Erlang/OTP 的
gen_server与 Skynet Service 的设计取舍;③ 读云风博客中关于 Skynet 协程调度的设计权衡;④ 回到 技术架构与社会哲学的镜像.md 中的框架,验证"架构 = 社会组织形态"在 Skynet 三层模型中的映射