skynet——RPC 超时机制优化方案

RPC 超时机制优化方案

本文档分析当前项目 RPC 超时机制的实现方式及其性能问题,并给出优化方案。

适合面试时作为「高并发系统性能优化」的话题展开。


一、当前实现回顾

1.1 核心代码

当前超时机制位于 lualib/rpc.lua 的 timed_call() 函数:

lua 复制代码
-- lualib/rpc.lua 第 209-229 行
local function timed_call(timeout_ms, target, ...)
    local co = coroutine.running()
    local session = {}
    local cancelled = false
    local packed = table.pack(...)

    -- ① fork 一个新协程执行阻塞调用
    skynet.fork(function()
        local ok, res = pcall(skynet.call, target, table.unpack(packed, 1, packed.n))
        if not cancelled then
            session.result = { ok, res }
            skynet.wakeup(co)       -- 调用成功 → 唤醒父协程
        end
    end)

    -- ② 父协程 sleep 等待超时
    local timeout_cs = math.max(1, math.ceil(timeout_ms / 10))
    skynet.sleep(timeout_cs)

    -- ③ 竞速判断:谁先到?
    if session.result then
        return table.unpack(session.result)   -- 调用先返回 → 成功
    else
        cancelled = true
        return false, "timeout"               -- sleep 先到 → 超时
    end
end

1.2 执行时序

复制代码
    父协程                              子协程 (由 fork 创建)
       │                                    │
       ├─ fork(子协程) ────────────────────→│
       │                                    ├─ skynet.call(target, ...)
       │                                    │    ↓ 发送请求消息
       │                                    │    ↓ coroutine_yield("SUSPEND")
       │                                    │    【阻塞,等待响应】
       │                                    │
       ├─ skynet.sleep(timeout_cs)          │
       │    ↓ coroutine_yield("SUSPEND")    │
       │    【阻塞,等待超时或唤醒】          │
       │                                    │
       │  ═══════ skynet 调度器调度 ═══════  │
       │                                    │
       ├─ (场景A) 被 wakeup 唤醒 ←──────────┤ 响应到达 → 设置 result → wakeup(父)
       │   返回 result                       │
       │                                    │
       ├─ (场景B) sleep 超时               │
       │   无 result → 设置 cancelled=true   │ (子协程最终返回但 result 被忽略)
       │   返回 "timeout"                    │

1.3 架构本质:fork + sleep 竞速

这不是一个"带超时的 RPC 调用",而是一个竞速模型:

  • 跑道 1 (fork 协程):执行 skynet.call,等待对端响应
  • 跑道 2(父协程 sleep):等待超时时间到期
  • 两个跑道的终点都是"唤醒父协程",谁先到谁说了算

二、性能问题分析

2.1 问题维度一:协程内存开销

每次 RPC 调用发出时,skynet.fork 会创建一个新的 Lua 协程(coroutine)。

在 Lua 5.4 中,每个协程的初始栈大小默认约 40KB。对于一个需要同时处理 10,000 个并发 RPC 的系统:

复制代码
10,000 并发 RPC × 40KB/coroutine = 400MB 额外内存占用

这还只是栈空间,不包括协程对象本身、闭包捕获的 upvalue、以及 table.pack(...) 捕获的参数。实际内存开销更高。

2.2 问题维度二:C 层定时器注册

skynet.sleep(timeout_cs) 每次调用都会在 skynet 的 C 层定时器模块中注册一个定时器事件:

c 复制代码
// skynet 内部定时器实现(skynet_timer.c)
// 每个 sleep 调用 → 向 timer_wheel 插入一个节点
// 10,000 个并发 RPC → 10,000 个定时器节点

skynet 的定时器采用时间轮(timer wheel)实现,虽然插入是 O(1) 的,但 10,000 个节点意味着:

  • 定时器 tick 时需要遍历更多槽位
  • 内存中的定时器节点占用(每个约几十字节)
  • GC 压力增大(定时器过期后需要回收 Lua 侧的闭包和表)

2.3 问题维度三:协程创建/销毁频率

在 RPC 密集型场景(如网关转发、缓存查询、微服务编排),RPC 调用频率可能达到每秒数千次。每秒创建/销毁数千个协程:

复制代码
协程创建: lua_newthread() → 分配栈空间 → 压入 fork_queue
协程销毁: GC 回收 → 释放栈空间

频繁的 lua_newthread + GC 回收会造成:

  • 内存分配器压力(频繁 malloc/free)
  • GC 停顿(大量短生命周期对象触发 GC)
  • CPU cache miss(新建的协程栈不在 cache 中)

2.4 问题量化总结

指标 当前方案(per-call fork+sleep) 影响
协程数 N(每个 RPC 创建 1 个) 内存 O(N),N 大时爆炸
C 定时器 N(每个 RPC 注册 1 个) 定时器轮压力 O(N)
协程创建 每次调用创建 1 次 高频 malloc + GC
上下文切换 父→子→父,至少 2 次 CPU 开销

三、优化方案:skynet.timeout + 最小堆(事件驱动)

3.1 为什么是 skynet.timeout 而不是 skynet.sleep?

先看两种 API 的本质区别:

skynet.sleep(ti) skynet.timeout(ti, func)
底层实现 auxtimeout(ti) 注册 C 定时器 auxtimeout(ti) 注册 C 定时器
定时器到期后 直接恢复原协程 派发消息 → 创建新协程执行 func
使用模式 "我睡 N 毫秒,别叫醒我"(阻塞式) "N 毫秒后通知我来做某事"(回调式)
适合场景 协程需要等待固定时间 需要响应式触发、可动态重新调度

对于看守者的场景,skynet.timeout 天然更优:

复制代码
sleep-loop 模式(轮询):
   while true → sleep(N) → 醒来检查 → sleep(N) → ...  
   像闹钟每 100ms 响一次,大部分是空跑

timeout 模式(事件驱动):
   设一个定时器指向「最早到期时间」→ 到期处理 → 设下一个
   像日程提醒,只在有事时响,没事就安静

3.2 核心思路

本质上和 sleep-loop 方案一样------用最小堆统一管理所有超时。但调度方式从"轮询"变成"事件驱动"。

类比:

复制代码
当前方案:    每个快递包裹配一个闹钟(10,000 个包裹 → 10,000 个闹钟)
sleep-loop:  一个前台管理员,每隔 100ms 看一眼所有包裹的到期时间
timeout 方案: 一个前台管理员,设一个智能闹钟指向最早到期时间
              → 闹钟响了 → 处理到期包裹 → 重新设闹钟 → 继续等待

3.3 关键难题:如何"取消" skynet.timeout?

skynet.timeout 没有 cancel API 。一旦调用了 skynet.timeout(ti, func),ti 毫秒后 func 一定会被执行。

但我们的看守者需要频繁「重设」定时器:新来一个更早到期的 RPC,旧的 timeout 就作废了。

解决方案:代数计数器(Generation Counter)

lua 复制代码
local generation = 0  -- 全局代数

-- 每次重设定时器时递增代数
generation = generation + 1
local my_gen = generation
skynet.timeout(wait_cs, function()
    if my_gen ~= generation then
        return  -- 代数落后了!说明已经被更新的 timeout 替代,直接跳过
    end
    -- 我是最新的 timeout,正常处理
    process_expired()
end)

这个技巧的本质是:旧 timeout 仍然会触发,但回调发现自己"过期了"就直接返回,相当于逻辑取消。

3.4 数据结构设计

复制代码
┌─────────────────────────────────────────────────┐
│              heap (最小堆, 按 deadline_ms 排序)   │
│  [{deadline: 100, co: 0x7f...A},                │
│   {deadline: 200, co: 0x7f...B},                │
│   {deadline: 350, co: 0x7f...C}, ...]           │
│    ↑ 堆顶:最早到期的 RPC                         │
└─────────────────────────────────────────────────┘

全局状态(仅 3 个变量):
  heap        → 最小堆
  generation  → 代数计数器(0 = 无活跃 timeout)
  has_pending → 是否有活跃的 timeout 回调

没有 sweeper 协程!没有无意义的 sleep-wake 循环! 状态完全由 timeout 驱动。

3.5 执行流程(时序图)

复制代码
  父协程 (业务)              子协程 (fork)              timeout 回调 (按需创建)
     │                         │                          │
     ├─1. deadline = now+200   │                          │
     ├─2. heap:push(deadline)  │                          │
     ├─3. 堆顶变了?             │                          │
     │   → schedule_next()     │                          │
     │   → generation++        │                          │
     │   → skynet.timeout(     │                          │
     │       200ms, callback)  │                          │
     │                         │                          │
     ├─4. fork ───────────────→│                          │
     │                         ├─ skynet.call(target)     │
     │                         │   ↓ SUSPEND              │
     ├─5. skynet.wait(co)      │                          │
     │   ↓ SUSPEND             │                          │
     │                         │                          │
     │  ─── 场景A: RPC 50ms 就返回了 (正常) ───           │
     │                         ├─ 响应到达, 协程恢复       │
     │  ← wakeup(parent) ──────┤                          │
     │                         ├─ heap:remove(parent)     │
     │                         │   (堆顶变了→schedule)     │
     ├─ 检查 result_data       │   generation++           │
     └─ 返回成功                │   skynet.timeout(新)     │
                               │                          │
     │  ─── 场景B: RPC 确实超时了 (200ms 后) ───          │
     │                         │                    ← timeout 触发
     │                         │                     my_gen == generation? ✓
     │                         │                     process_expired():
     │                         │                       while heap:peek() <= now:
     │                         │                         pop → wakeup(co)
     │  ← wakeup ──────────────│─────────────────────┤
     ├─ result_data == nil     │                     generation = 0
     └─ 返回 "timeout"         │

3.6 完整代码实现

lua 复制代码
-- ============================================
-- rpc_timeout.lua: skynet.timeout + 最小堆 超时管理器
-- 用代数计数器实现 timeout 的"逻辑取消"
-- ============================================

local skynet = require "skynet"

local M = {}

-- ============================================
-- Min-Heap (按 deadline_ms 排序)
-- ============================================
local Heap = {}
Heap.__index = Heap

function Heap.new()
    return setmetatable({ data = {}, size = 0 }, Heap)
end

function Heap:push(deadline_ms, coroutine_ref)
    self.size = self.size + 1
    self.data[self.size] = { deadline = deadline_ms, co = coroutine_ref }
    local i = self.size
    while i > 1 do
        local p = math.floor(i / 2)
        if self.data[p].deadline <= self.data[i].deadline then break end
        self.data[p], self.data[i] = self.data[i], self.data[p]
        i = p
    end
end

function Heap:pop()
    if self.size == 0 then return nil end
    local top = self.data[1]
    self.data[1] = self.data[self.size]
    self.data[self.size] = nil
    self.size = self.size - 1
    local i = 1
    while true do
        local left, right = i * 2, i * 2 + 1
        local smallest = i
        if left <= self.size and self.data[left].deadline < self.data[smallest].deadline then
            smallest = left
        end
        if right <= self.size and self.data[right].deadline < self.data[smallest].deadline then
            smallest = right
        end
        if smallest == i then break end
        self.data[i], self.data[smallest] = self.data[smallest], self.data[i]
        i = smallest
    end
    return top.deadline, top.co
end

function Heap:peek()
    if self.size == 0 then return nil, nil end
    return self.data[1].deadline, self.data[1].co
end

function Heap:remove(coroutine_ref)
    for i = 1, self.size do
        if self.data[i].co == coroutine_ref then
            self.data[i] = self.data[self.size]
            self.data[self.size] = nil
            self.size = self.size - 1
            local idx = i
            while true do
                local left, right = idx * 2, idx * 2 + 1
                local smallest = idx
                if left <= self.size and self.data[left].deadline < self.data[smallest].deadline then
                    smallest = left
                end
                if right <= self.size and self.data[right].deadline < self.data[smallest].deadline then
                    smallest = right
                end
                if smallest == idx then break end
                self.data[idx], self.data[smallest] = self.data[smallest], self.data[idx]
                idx = smallest
            end
            return true
        end
    end
    return false
end

-- ============================================
-- 超时管理器(事件驱动,无 sweeper 协程)
-- ============================================

local heap = Heap.new()
local generation = 0    -- 代数计数器: 0 = 无活跃 timeout, >0 = 当前 timeout 代数

-- 处理所有已过期的 RPC,然后调度下一个 timeout
local function process_expired()
    local now = skynet.now()
    while true do
        local deadline, co = heap:peek()
        if not deadline or deadline > now then break end
        heap:pop()
        if co then
            skynet.wakeup(co)
        end
    end
    -- 处理完后,如果还有待处理的,调度下一个 timeout
    schedule_next()
end

-- 为堆顶(最早到期)的 RPC 设一个 timeout
function schedule_next()
    local deadline = heap:peek()
    if not deadline then
        generation = 0  -- 没有待处理的超时,清空代数
        return
    end

    local now = skynet.now()
    local wait_ms = deadline - now
    if wait_ms <= 0 then
        -- 已经过期,立刻处理(但不在当前调用栈中,避免递归过深)
        skynet.timeout(0, process_expired)
        return
    end

    -- ★ 递增代数,使旧的 timeout 回调自动失效
    generation = generation + 1
    local my_gen = generation

    local wait_cs = math.max(1, math.ceil(wait_ms / 10))
    skynet.timeout(wait_cs, function()
        -- ★ 代数检查:代数和注册时不一致 → 已被新 timeout 替代 → 跳过
        if my_gen ~= generation then
            return
        end
        process_expired()
    end)
end

-- 注册超时(由 timed_call 调用)
function M.register(deadline_ms)
    local co = coroutine.running()
    local old_earliest = heap:peek()
    heap:push(deadline_ms, co)
    -- 如果新插入的是最早到期的(或之前堆为空),重新调度
    local new_earliest = heap:peek()
    if new_earliest == deadline_ms or not old_earliest then
        schedule_next()
    end
end

-- 取消超时(RPC 成功返回时调用)
function M.cancel(coroutine_ref)
    local was_earliest = (heap:peek() == coroutine_ref)
    heap:remove(coroutine_ref)
    if was_earliest then
        -- 堆顶被移除了,需要重新调度到新的堆顶
        schedule_next()
    end
end

-- ============================================
-- 带超时的 RPC 调用
-- ============================================
function M.timed_call(timeout_ms, target, ...)
    local parent_co = coroutine.running()
    local packed = table.pack(...)
    local cancelled = false
    local result_data = nil
    local deadline_ms = skynet.now() + timeout_ms

    -- 1. 向看守者注册超时
    M.register(deadline_ms)

    -- 2. fork 子协程执行实际调用(fork 无法消除,见第五章分析)
    skynet.fork(function()
        local ok, res = pcall(skynet.call, target, table.unpack(packed, 1, packed.n))
        M.cancel(parent_co)
        if not cancelled then
            result_data = { ok, res }
            skynet.wakeup(parent_co)
        end
    end)

    -- 3. 等待唤醒(来自子协程或 timeout 回调)
    skynet.wait(parent_co)

    -- 4. 判断结果
    if result_data then
        return table.unpack(result_data)
    else
        cancelled = true
        return false, "timeout"
    end
end

return M

3.7 与 rpc.lua 的集成

只需修改 lualib/rpc.lua 中的 timed_call 函数:

lua 复制代码
-- 在 rpc.lua 顶部引入
local rpc_timeout = require "rpc_timeout"

-- 替换原有的 timed_call 函数
local function timed_call(timeout_ms, target, ...)
    return rpc_timeout.timed_call(timeout_ms, target, ...)
end

其他代码(local_call、remote_call、M.call)无需任何修改。

3.8 代数计数器原理图解

复制代码
时间线 ───────────────────────────────────────────────────→

RPC-A 注册 (deadline=200ms):
  generation: 0→1    timeout(200ms, cb[gen=1]) 注册
                                              │
RPC-B 注册 (deadline=100ms, 更早!):              │
  generation: 1→2    timeout(100ms, cb[gen=2]) 注册
                     │                         │
                     │    ← cb[gen=2] 触发     │
                     │    gen==2 ✓ 执行!       │
                     │    处理过期RPC...         │
                     │    堆顶变成 RPC-A(200ms)  │
                     │    generation: 2→3       │
                     │    timeout(100ms, cb[gen=3])  │
                     │                         │
                     │              ← cb[gen=1] 触发
                     │              gen=1≠3 ✗ 跳过! (已被更晚注册的B替代过)
                     │                         │
                     │    ← cb[gen=3] 触发     │
                     │    gen==3 ✓ 执行!       │
                     │    处理 RPC-A 超时       │

关键:cb[gen=1] 虽然物理上仍然触发了,但它检查 my_gen ~= generation 后直接 return,相当于零成本逻辑取消。


四、优化效果对比

4.1 sleep-loop 方案 vs timeout 方案(先对比两种优化路线)

维度 sleep-loop(文档旧版) skynet.timeout + 代数计数器(推荐)
活跃协程 1 个 sweeper(永久存在) 0 个(timeout 回调按需创建)
定时器行为 周期性 sleep→醒来→sleep 按最早到期时间精准触发
空跑次数 多(每次 sleep 到期不一定有事) 零(只在真正到期时触发)
响应精度 取决于 sleep 间隔(最低 10ms) 最高精度(直接指向 deadline)
无 RPC 时 仍然每 100ms sleep 一次 零开销(generation=0,无任何 timer)
架构风格 轮询(polling) 事件驱动(event-driven)
取消机制 不需要(sweeper 跳到下一个) 代数计数器(旧 callback 逻辑跳过)

4.2 三种方案资源消耗对比

指标 当前方案 (fork+sleep) sleep-loop 优化 timeout+heap 优化 最优改善
协程数(额外) N(每个 RPC fork) N+1(+1 sweeper) N(只有 fork) 省掉 sweeper
C 层定时器数 N 个 1 个(sweeper sleep) 0~1 个(按需 timeout) ↓ O(N)→O(1)
空跑定时器触发 N/A 有(sleep 周期唤醒) 无 ✓
定时器精度 deadline ~sleep_interval 精确到 deadline ✓
无待处理 RPC 时 N/A 1 次/秒 sleep 0 开销 ✓
堆操作 无 insert O(log N) + remove O(N) insert O(log N) + remove O(N) 相同

4.3 正常路径性能(RPC 成功,无超时)

假设一个网关节点每秒处理 5000 个 RPC 调用 ,平均耗时 50ms ,超时设置 200ms:

指标 当前方案 timeout+heap 方案
并发 fork 协程 ~250 ~250(一样)
活跃 C 定时器 ~250 0~1(timeout 只在成功时被 cancel 触发重调度)
每秒 C 定时器注册 5000 次 ~250 次(每次 RPC 成功 cancel 触发 1 次 schedule_next)
timeout 回调创建 N/A(用 sleep) 仅 RPC 超时时创建(<1% 的情况)
堆 remove O(N) 扫描 无 有(线性扫描找 coroutine),可优化为 hash+heap 降到 O(log N)

结论 :timeout 方案在最常见的「RPC 正常返回」路径上,C 定时器注册从 5000 次/秒降到 ~250 次/秒(95% 减少)。如果没有任何 RPC 超时,timeout 回调一次都不会创建。

4.4 极端场景:超时风暴(timeout 方案的亮点)

当后端服务故障导致大量 RPC 同时超时:

当前方案:

  • 10,000 个 skynet.sleep 几乎同时到期
  • skynet 定时器 tick 需要处理 10,000 个回调
  • 可能造成定时器 tick 处理时间过长,阻塞消息循环

sleep-loop 方案:

  • sweeper 醒来后 while 循环批量处理
  • 定时器侧只有 1 次回调解耦

timeout+heap 方案(最优):

  • process_expired() 同样批量处理,这和 sleep-loop 一样

  • 但额外优势:处理粒度更细

    lua 复制代码
    -- process_expired 内部可以加 yield 分摊压力
    local function process_expired()
        local now = skynet.now()
        local batch = 0
        while true do
            local deadline, co = heap:peek()
            if not deadline or deadline > now then break end
            heap:pop()
            skynet.wakeup(co)
            batch = batch + 1
            if batch % 100 == 0 then
                skynet.yield()  -- ★ 每 100 个让出一次,避免饿死其他消息
            end
        end
        schedule_next()
    end
  • 配合 generation 机制,超时风暴期间新的 timeout 注册不会被冲掉


五、进阶优化:消除 fork(终局方案)

5.1 为什么 fork 仍需保留?

因为 skynet.call 是阻塞式 的------它在内部调用 coroutine_yield("SUSPEND") 并等待响应。必须有一个独立的协程来执行这个阻塞调用,父协程才能同时等待超时。

复制代码
父协程不能既"阻塞在 skynet.call 中"又"等待超时" ------ 这是互斥的。
fork 是绕过这个限制的唯一手段。

5.2 彻底消除 fork 的条件

要完全消除 fork,需要 skynet 层面支持 带超时的 call。有两种方案:

方案 A:修改 skynet C 层

在 skynet_call 的 C 实现中增加超时参数,当超时到期时,定时器回调直接向 watching_session 注入一个错误响应:

c 复制代码
// 伪代码
int skynet_call_with_timeout(addr, type, msg, sz, timeout_ms) {
    int session = skynet_send(addr, type, msg, sz);
    register_timeout(session, timeout_ms, on_timeout_callback);
    return yield_call(addr, session);
}

void on_timeout_callback(int session) {
    // 模拟一个 PTYPE_ERROR 响应,让 yield_call 以错误状态返回
    inject_response(session, PTYPE_ERROR, "timeout");
}

方案 B:使用 skynet.send + 自定义响应路由

不调用 skynet.call,而是用 skynet.send 发送请求,然后手动管理响应等待:

lua 复制代码
-- 伪代码
local pending = {}  -- session → { co, deadline }

skynet.register_protocol({
    id = skynet.PTYPE_RESPONSE,
    unpack = skynet.tostring,
    dispatch = function(session, _, response)
        local info = pending[session]
        if info then
            pending[session] = nil
            info.result = response
            skynet.wakeup(info.co)
        end
    end
})

function timed_call_no_fork(timeout_ms, target, ...)
    local session = skynet.send(target, "lua", ...)  -- 非阻塞
    local co = coroutine.running()
    pending[session] = { co = co, deadline = skynet.now() + timeout_ms }
    timeout_heap:push(session)
    
    skynet.wait(co)  -- 等待 wakeup
    
    local info = pending[session]
    if info.result then
        return info.result
    else
        return false, "timeout"
    end
end

⚠️ 方案 B 需要确认 skynet 的 PTYPE_RESPONSE 是否可以被自定义协议拦截。


六、面试要点总结

当面试官问「你做过什么性能优化」时,可以这样展开:

开场(30 秒)

"我所在项目的 RPC 超时机制原本是「每个 RPC 调用创建一个 fork 协程 + 一个 sleep 定时器」,高并发下 C 层定时器节点膨胀到 O(N)。我把它优化成了 skynet.timeout + 最小堆 + 代数计数器 的事件驱动方案,定时器从 O(N) 降到 O(1),无待处理 RPC 时零定时器开销。"

展开细节(2 分钟)

  1. 问题定位 :压测发现 QPS 上不去,profile 追到 skynet_timer_update CPU 占比异常,原因是每个带超时的 RPC 都注册了独立 C 层定时器------N 个并发 RPC = N 个 timer_node。

  2. 根因分析 :fork + sleep 竞速模型中,skynet.sleep 每次调用都在 C 层 timer_wheel 上挂一个节点。10,000 并发 = 10,000 个节点,虽然插入 O(1),但 tick 轮转时的遍历和内存开销不可忽略。

  3. 核心思路 :用最小堆集中管理所有 RPC 的到期时间,仅对堆顶(最早到期的那个)注册一个 skynet.timeout。到期后批量 pop 处理,再对新的堆顶设下一个 timeout。

  4. 关键技巧------代数计数器 :skynet.timeout 没有 cancel API,但堆顶频繁变化需要「取消旧 timeout、设新 timeout」。我用一个全局 generation 计数器,每次重设 timeout 时递增。旧 timeout 回调触发后第一行检查 my_gen == generation,不等就直接 return------相当于 O(1) 的逻辑取消,不需要修改 C 层。

  5. 为什么是 timeout 而不是 sleep-loop :sleep-loop 本质是轮询(sweeper 每 100ms 醒来检查),有空跑开销。timeout 是事件驱动------只在真正到期时触发,无 RPC 时 generation=0,零定时器开销。

  6. 效果 :C 定时器注册从 5000 次/秒降到 ~250 次/秒,无 RPC 时 0 个活跃定时器。超时风暴场景下可以用 skynet.yield() 在 process_expired 中分批处理,不阻塞消息循环。

可能的追问

Q: 为什么不用时间轮而用最小堆?

A: 时间轮适合「超时集中在几个固定值」的场景(如连接超时统一 30s)。但 RPC 超时粒度可能从 200ms 到 30s 不等,时间轮要么槽太粗丢失精度,要么槽太细内存浪费。最小堆 O(log N) 的插入在 N<10000 时完全不构成瓶颈,且实现极简(~50 行)。

Q: 代数计数器有没有 ABA 问题?

A: generation 用 Lua number(double,53 位整数精度),即使每秒重设 10 万次 timeout,也要跑 285 万年才会溢出回绕。实践中完全安全。

Q: 旧的 timeout 回调虽然跳过了,但 C 层定时器仍然触发了,这是浪费吗?

A: 是,但代价极小------C 定时器已经注册了,物理触发不可避免。回调第一行就是 if 判断 + return,一个 Lua 条件跳转的代价约几十纳秒,远小于 fork 一个协程的开销。而且只有堆顶变化频繁时才有多余触发,正常路径上一次 timeout 就够了。

Q: 为什么 fork 消除不了?

A: skynet.call 内部通过 coroutine_yield("SUSPEND") 阻塞等待响应,同一个协程不能既阻塞在 call 里又同时等超时。消除 fork 需要改 skynet C 层(在 yield_call 中注入超时错误响应),或改用 skynet.send + 自定义 PTYPE_RESPONSE 拦截------后者会侵入 skynet 内部协议路由。当前 Lua 层方案已经是能做到的最优解。


七、参考资料

  • skynet 定时器实现:skynet/skynet-src/skynet_timer.c(时间轮 + 层级降级)
  • skynet 协程调度:skynet/lualib/skynet.lua 的 yield_call、suspend、fork_queue
  • 当前 RPC 实现:chatroom/lualib/rpc.lua 的 timed_call 函数
  • 最小堆参考:算法导论第 6 章(Binary Heap)
相关推荐
笨鸟先飞的橘猫2 小时前
skynet热更新实现问题与排错记录
学习·lua·skynet
9624561 天前
Go EVM 链上数据平台实践:从 RPC Demo 到可演进架构
rpc·架构·golang
奥列格1 天前
《神探杰克:导演剪辑版》steam Windows版汉化补丁
游戏
全速向光1 天前
Game Cheats Manager 使用教程:开源的单机游戏修改器聚合管理方案
游戏
河南花仙子科技2 天前
古镇非遗小游戏如何传播传统文化
游戏
梦想平凡2 天前
棋牌游戏技术实战(十三):缓存对局历史时控制容量,命中以后仍然检查成员权限
游戏·缓存
阿阳微客2 天前
steam游戏搬砖用选品软件照搬就可以么?经验帖
学习·游戏
FairGuard手游加固3 天前
手游加固怎么做?SO、IL2CPP、AssetBundle 与运行时保护详解
安全·游戏·unity
Vex2une3 天前
GRLP游戏运行库合集:一站式解决DLL缺失与启动报错实战指南 | Game Runtime Libraries Package
游戏·vc++运行库·0xc000007b·游戏运行库·dll缺失修复·directx修复·游戏报错解决