作用
skynet.timeout(ti, func) 用来延迟执行一次函数。
当前服务端实现位置:
tlbb/framework/lualib/skynet.lua
核心实现:
function skynet.timeout(ti, func)
local session = auxtimeout(ti)
assert(session)
local co = co_create_for_timeout(func, ti)
assert(session_id_coroutine[session] == nil)
session_id_coroutine[session] = co
return co -- for debug
end
时间单位
skynet.timeout 的时间单位是 1/100 秒。
也就是:
1 tick = 10 毫秒
100 tick = 1 秒
示例:
skynet.timeout(100, function()
print("1秒后执行")
end)
skynet.timeout(50, function()
print("0.5秒后执行")
end)
skynet.timeout(5, function()
print("0.05秒后执行")
end)
skynet.timeout(0, function()
print("下一轮调度执行")
end)
基本特性
- 只执行一次
skynet.timeout(100, function()
print("只执行一次")
end)
如果需要循环,必须在回调里再次注册:
local function loop()
print("每秒执行一次")
skynet.timeout(100, loop)
end
skynet.timeout(100, loop)
- 不阻塞当前逻辑
print("A")
skynet.timeout(100, function()
print("B")
end)
print("C")
执行顺序是:
A
C
约1秒后 B
- 回调在当前服务中执行
skynet.timeout 不是开新系统线程。
它会注册一个 timer,到期后给当前服务投递一个消息,然后由当前服务调度执行回调。
大致流程:
注册 timer
↓
timer 到期
↓
向当前服务投递 response 消息
↓
恢复对应 coroutine
↓
执行 func()
和 skynet.sleep 的区别
skynet.timeout 是异步注册一个延迟回调:
skynet.timeout(100, function()
do_something()
end)
print("不会等待 timeout")
skynet.sleep 是挂起当前 coroutine:
skynet.sleep(100)
do_something()
区别:
skynet.timeout:当前逻辑继续往下走,回调以后再执行。
skynet.sleep:当前 coroutine 暂停,到时间后继续往下走。
适合使用 skynet.timeout 的场景:
延迟补发状态
延迟清理对象
定时刷新缓存
分批处理任务
循环检查状态
适合使用 skynet.sleep 的场景:
当前流程必须等待一段时间后继续
性能影响
少量使用没有问题。
例如:
skynet.timeout(100, function()
self:RefreshSomething(selfId)
end)
这种登录后补发一次状态的开销很低。
但 skynet.timeout 不是零成本。每次调用都会产生:
一个 timer session
一个 coroutine
一次到期后的消息投递
一次回调执行
所以大量使用会造成压力。
例如不推荐:
for _, player in pairs(players) do
skynet.timeout(100, function()
heavy_refresh(player)
end)
end
如果玩家很多,一秒后会集中触发大量回调,造成服务瞬时压力。
更好的方式是分批:
local function process_batch(index)
local last = math.min(index + 50, #players)
for i = index, last do
process_player(players[i])
end
if last < #players then
skynet.timeout(1, function()
process_batch(last + 1)
end)
end
end
process_batch(1)
常见风险
- 玩家或对象已经不存在
错误写法:
local human = self:GetHuman(selfId)
skynet.timeout(100, function()
human:notify_tips("提示")
end)
风险:
1 秒后玩家可能已经下线,human 可能已经失效。
推荐写法:
skynet.timeout(100, function()
local human = self:GetHuman(selfId)
if not human then
return
end
human:notify_tips("提示")
end)
- 捕获了过期状态
错误写法:
local level = self:GetLevel(selfId)
skynet.timeout(100, function()
if level >= 50 then
self:DoSomething(selfId)
end
end)
风险:
1 秒后玩家等级、状态、任务数据可能已经变化。
推荐写法:
skynet.timeout(100, function()
if (self:GetLevel(selfId) or 0) >= 50 then
self:DoSomething(selfId)
end
end)
- 无限制循环
危险写法:
local function loop()
do_something()
skynet.timeout(1, loop)
end
loop()
风险:
每 0.01 秒执行一次,如果逻辑稍重,会持续占用服务。
推荐写法:
local running = true
local function loop()
if not running then
return
end
do_something()
skynet.timeout(100, loop)
end
skynet.timeout(100, loop)
- 不能直接当作可取消定时器
skynet.timeout 返回的是 coroutine,注释也是:
return co -- for debug
不要把它当成稳定的取消句柄。
如果需要“取消”,推荐使用版本号或状态标记。
示例:
self.refresh_version = (self.refresh_version or 0) + 1
local version = self.refresh_version
skynet.timeout(100, function()
if version ~= self.refresh_version then
return
end
self:RefreshSomething()
end)
- 热重载后的旧回调
脚本热重载前注册的 timeout,到期后仍可能执行旧闭包。
因此回调中不要依赖复杂旧状态。
推荐只捕获 ID,然后重新查询当前状态:
local objId = selfId
skynet.timeout(100, function()
local human = self:GetHuman(objId)
if not human then
return
end
self:RefreshSomething(objId)
end)
- 捕获大型 table
错误写法:
local big_table = BuildBigTable()
skynet.timeout(100, function()
UseBigTable(big_table)
end)
风险:
big_table 会被闭包持有,直到回调执行完才释放。
推荐写法:
local id = data.id
skynet.timeout(100, function()
local data = LoadDataById(id)
if not data then
return
end
UseData(data)
end)
推荐写法模板
延迟执行一次:
skynet.timeout(100, function()
local human = self:GetHuman(selfId)
if not human then
return
end
self:DoSomething(selfId)
end)
带 GUID 校验:
local human = self:GetHuman(selfId)
if not human then
return
end
local guid = human.get_guid and human:get_guid() or nil
skynet.timeout(100, function()
local cur_human = self:GetHuman(selfId)
if not cur_human then
return
end
if guid and cur_human.get_guid and cur_human:get_guid() ~= guid then
return
end
self:DoSomething(selfId)
end)
循环任务:
local running = true
local function loop()
if not running then
return
end
self:DoSomething()
skynet.timeout(100, loop)
end
skynet.timeout(100, loop)
版本取消:
self.refresh_version = (self.refresh_version or 0) + 1
local version = self.refresh_version
skynet.timeout(100, function()
if version ~= self.refresh_version then
return
end
self:DoSomething()
end)
什么时候适合用
适合:
登录后延迟补发一次状态
副本中延迟刷怪
怪物死亡后延迟清尸体
技能读条后执行结果
定时刷新缓存
分批处理大量数据
短周期但数量少的状态检查
不适合:
每个玩家都开高频循环
每个怪物都开高频循环
大量定时器同一时间触发
需要精确取消、暂停、恢复的复杂计时系统
回调里执行重逻辑
回调里长期持有对象引用
对性能的判断标准
可以用:
少量、一次性、轻逻辑
谨慎用:
大量玩家同时注册
大量对象同时注册
短间隔循环
回调里有数据库、全表遍历、复杂计算
不要用:
每 0.01 秒跑一次重逻辑
每个怪物都挂一个循环 timeout
大批量同一 tick 到期执行重逻辑
和英雄归来图标问题的关系
英雄归来图标最终修复没有使用 skynet.timeout。
原因是当前测试确认:
self:DispatchQuickEnterPoint(selfId, 0, 3, flag)
在登录流程中同步下发即可正常隐藏图标。
所以最终版本不需要:
skynet.timeout(100, function()
self:RefreshQuickEnterState(selfId)
end)
只有在确实遇到“客户端初始化晚于服务端发包,导致状态被覆盖”的情况时,才考虑用 timeout 做延迟补发。当前这个问题已经不需要。