Lua 协程:从 API 到底层原理再到 Skynet 架构的完整学习路径

📄 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.yieldskynet.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 的完整过程:

  1. Service A 的协程 C1 把消息放进 B 的消息队列 → coroutine.yield() → C1 挂起
  2. 某 Worker 拿到 B 的 lua_State → fork 协程 C2 → C2 处理请求 → skynet.ret(result)
  3. skynet.ret 找到 A 中挂起的 C1 → 把 result push 到 A 的 lua_State
  4. Worker 拿到 A 的 lua_Statecoroutine.resume(C1, result)
  5. 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、Go goroutine、Rust async,只需对比语义差异
  • Actor+协程的融合架构是分布式系统的标准答案:Erlang/OTP、Akka、Orleans、Skynet 无不采用此模式。微服务、Kubernetes、Serverless 在更粗粒度上重复同一逻辑------消息驱动 + 无共享 + 故障隔离
  • 用户态调度(M:N 模型)是高性能并发的关键:Skynet 的 Worker 线程复用模型等价于 Go 的 GMP 调度器------把协程调度留在用户态,不走内核上下文切换。未来设计高性能系统,这是一项核心判断力
  • 建议后续深入方向 :① 在 Skynet 实战中刻意体会"消息驱动"和"共享内存驱动"两种思维的差异;② 对比 Erlang/OTP 的 gen_server 与 Skynet Service 的设计取舍;③ 读云风博客中关于 Skynet 协程调度的设计权衡;④ 回到 技术架构与社会哲学的镜像.md 中的框架,验证"架构 = 社会组织形态"在 Skynet 三层模型中的映射
相关推荐
2601_9574188015 小时前
相机如何连接手机?通俗易懂的PTP/MTP连接原理解析
android·数码相机·架构
段一凡-华北理工大学15 小时前
工业领域的Hadoop架构学习~系列文章01:Hadoop与工业4.0深度融合
大数据·hadoop·学习·架构·知识图谱·高炉炼铁·工业智能体
千寻girling15 小时前
机器学习 | 监督学习算法(了解) | 尚硅谷学习
开发语言·人工智能·后端·python·学习·算法·机器学习
red_redemption15 小时前
自由学习记录(195)
学习
向日的葵00615 小时前
Redis后端分布式与高并发架构演进
redis·分布式·架构
wb0430720115 小时前
架构是“长“出来的
adb·架构
“码”力全开15 小时前
容器化架构下的边缘计算:基于Docker与GB28181/RTSP多协议汇聚的AI视频管理平台架构解析与源码交付实践
人工智能·架构·边缘计算
该用户已躺平@15 小时前
并网逆变器学习笔记11---并网逆变器学习---SSRF-PLL、DDSRF-PLL、DSOGI-PLL快速上手(结合deepseek脚本快速生成)
笔记·学习
宠友信息15 小时前
友猫社区Vue与Spring Boot多端社交平台源码架构
java·vue.js·spring boot·架构