找回密码
 register
搜索
查看: 31|回复: 0

洱海月 skynet.timeout 教程

[复制链接]
  • 打卡等级:本地老炮
  • 打卡总天数:553
  • 打卡月天数:14
  • 打卡总奖励:550
  • 最近打卡:2026-07-14 06:29:25
Waylee 发表于 2026-7-4 02:22 | 显示全部楼层 |阅读模式 | Google Chrome | Windows 10

马上注册,查看网站隐藏内容!!

您需要 登录 才可以下载或查看,没有账号?register

×

作用

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)

基本特性

  1. 只执行一次
skynet.timeout(100, function()
    print("只执行一次")
end)

如果需要循环,必须在回调里再次注册:

local function loop()
    print("每秒执行一次")
    skynet.timeout(100, loop)
end

skynet.timeout(100, loop)
  1. 不阻塞当前逻辑
print("A")

skynet.timeout(100, function()
    print("B")
end)

print("C")

执行顺序是:

A
C
约1秒后 B
  1. 回调在当前服务中执行

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)

常见风险

  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)
  1. 捕获了过期状态

错误写法:

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)
  1. 无限制循环

危险写法:

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)
  1. 不能直接当作可取消定时器

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)
  1. 热重载后的旧回调

脚本热重载前注册的 timeout,到期后仍可能执行旧闭包。

因此回调中不要依赖复杂旧状态。
推荐只捕获 ID,然后重新查询当前状态:

local objId = selfId

skynet.timeout(100, function()
    local human = self:GetHuman(objId)
    if not human then
        return
    end

    self:RefreshSomething(objId)
end)
  1. 捕获大型 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 做延迟补发。当前这个问题已经不需要。

您需要登录后才可以回帖 登录 | register

本版积分规则

QQ|手机版|雪舞知识库 ( 浙ICP备15015590号-1 | 萌ICP备20232229号|浙公网安备33048102000118号 )|天天打卡

GMT+8, 2026-7-14 10:08 , Processed in 0.074305 second(s), 29 queries .

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表