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 分钟)
-
问题定位 :压测发现 QPS 上不去,profile 追到
skynet_timer_updateCPU 占比异常,原因是每个带超时的 RPC 都注册了独立 C 层定时器------N 个并发 RPC = N 个 timer_node。 -
根因分析 :
fork + sleep竞速模型中,skynet.sleep每次调用都在 C 层 timer_wheel 上挂一个节点。10,000 并发 = 10,000 个节点,虽然插入 O(1),但 tick 轮转时的遍历和内存开销不可忽略。 -
核心思路 :用最小堆集中管理所有 RPC 的到期时间,仅对堆顶(最早到期的那个)注册一个
skynet.timeout。到期后批量 pop 处理,再对新的堆顶设下一个 timeout。 -
关键技巧------代数计数器 :
skynet.timeout没有 cancel API,但堆顶频繁变化需要「取消旧 timeout、设新 timeout」。我用一个全局generation计数器,每次重设 timeout 时递增。旧 timeout 回调触发后第一行检查my_gen == generation,不等就直接 return------相当于 O(1) 的逻辑取消,不需要修改 C 层。 -
为什么是 timeout 而不是 sleep-loop :sleep-loop 本质是轮询(sweeper 每 100ms 醒来检查),有空跑开销。timeout 是事件驱动------只在真正到期时触发,无 RPC 时 generation=0,零定时器开销。
-
效果 :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)