ToLua Timer机制

Posted wmalloc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ToLua Timer机制相关的知识,希望对你有一定的参考价值。

 

从一个Bug说起:

在内部试玩时发现有个任务的玩家跟随Npc逻辑挂了.

telnet连接到出问题的设备上, 开始搞事情

这个跟随的逻辑是一个Timer驱动的. 这个Timer在主角创建时就会启动. 一开始不认为是Timer本身的问题. 怀疑是流程上各个地方有return的情况

动态改掉几个函数都没有执行到. 说明Timer的回调没有调用到. 怀疑到Timer本身来

输出了下Timer的状态, 发现是running = true. 感觉很诧异. 又看了下Timer里的其他字段发现有个handle. 输出来看了下, 发现有个removed = true.

这里就奇怪了, 说明被异常removed了. 于是开始看Timer的实现:

 

Timer.lua 创建一个Timer的流程如下:

1. Timer.New()

  创建元表, 基础字段初始化. running = false...

2. Timer.Start()

  1). 从UpdateBeat创建一个Handle

  通过调用UpdateBeat:CreateListener, 传入Timer.Update函数和Timer本身得到返回值Handle, 其作用是一个链表的Node.

  代码: self.handle = UpdateBeat:CreateListener(self.Update, self)

  2). 注册到UpdateBeat里:

  即把handler作为链表的node放到UpdateBeat的列表里.

  代码: UpdateBeat:AddListener(self.handle)

    3). 此时Timer类的初始化过程完成了. 后续就等着Timer.Update被调用了. 可以看到主要的内容其实是在UpdateBeat里.

  4). 要了解Handle的管理和Timer.Update的调用方式就要看UpdateBeat代码了

  5). UpdateBeat是在event.lua里定义的

       技术图片

   可以看到是一个_event的元表

  6) 关注下_event的内容:

  #1 _event.CreateListener(func, obj)

  即上面 1)里调用的函数, 这里的func就是Timer.Update, obj就是Timer自身.

  这里判断了self.keepSafe, 即5)里 定义UpdateBeat的第二个参数true. 所以这里的self.keepSafe = true.

  可以看到keepSafe的情况下, func和obj被包到了一个xfunctor里, 这个xfunctor看了下, 是一个xpcall的封装. 也就是keepSafe的event实际调用func时会用xpcall的方式执行. 出错的话会返回抛错信息. 

  然后是其他的几个字段. 包括缓存了封装了Timer.Update的xpcall函数的value字段. 实现双向链表需要的两个指针(_prev, _next), 和我们要查bug需要关注的removed = true . 可以看到在Timer.Start申请Handle之初, 这个removed就是true状态的.

            技术图片   

            技术图片

  #2_event.AddListener

  创建了Handle之后, 通过AddListener接口注册到链表里. 可以看到, 这里判断了self.lock 这个字段的作用是锁self.list. 即避免在操作list的时候, 有其他逻辑来改这个list.

  这个lock字段是在执行一轮Update时置为true. 执行完成一轮后改成false. 在lock为true的过程中, 如果有其他AddListener的行为, 都会把Add行为缓存到一个临时列表: self.opList里. 而不是self.list

  这个列表缓存了Add操作. 即function() self.list:pushnode(handle) end. 然后等执行一轮Update之后, 把缓存的Add的操作都执行一遍. (RemoveListener同理) 

            技术图片

  #3 _event.__call

  这个函数是在对_event执行调用操作时执行的. 如下图 UpdateBeat() 

            技术图片

  __call函数首先上锁 lock = true. 然后用迭代器生成器 ilist 遍历 链表list. 这个链表就是我们AddListener操作存放Timer的双向链表. (ilist详细见下方解释)

  每次迭代的 i 即为一个handle, f是xpcall包装的Timer.Update函数. 然后调用f函数, 返回xpcall的结果 成功标识: flag, 错误信息 msg. 如果出错, 则用error抛出LuaException. 并且调用_list.remove,从链表移除该Node, 并且解锁: lock = false

  (这里有个问题, 如果其中一个Timer出错了, 然后解锁了, 后面有Timer真的塞了Timer进来, 岂不是要炸?? 求解释. )

  如果一切正常情况下, 会在最后把cache的Add/RemoveListener的操作执行一遍.

            技术图片

  ilist迭代器可以在list.lua中看到, 根据Lua的for语句的性质. 迭代器生成器返回三个值: 迭代函数(list.next), 恒定状态(即_list), 控制变量(没用). 然后以恒定状态和控制变量为参数去调用迭代函数, 得到的返回值再赋值给 for循环的key, value (<var-list>)

  所以迭代函数为list.next. 这个函数只需要一个恒定状态(第一次是list, 其实就是头节点, 后面是每个node)即可了, 因为直接可以通过next访问到下一个. 当下一个就是头结点时, 循环结束.

  for循环定义:

   for <var-list> in <exp-list> do

list.lua:

            技术图片

            技术图片

2. 看了那么多代码, 发现控制removed = true的地方有两处: CreateListener时和执行list:remove时.
 考虑到Create是一开始调用的, 不可能一开始就挂了, 所以应该是list.remove的时候, 再看调用list.remove的地方, 有两处, 一处是RemoveListener, 一处是 _event.__call中遍历执行Update时,出错了会执行remove.
 第一处, RemoveListener时, Timer.handle.running肯定是false的. 所以肯定是第二处了, 第二处是xpcall调用的, 出问题肯定有LuaException抛出. 但是查了抛错Dump网站发现没有该用户的报错日志.
 这就尴尬了..... 后面考虑到我们做了灰度测试开关, 只有部分用户的报错信息会上传, 于是找了另外几个出问题的账号, 终于发现有个用户在反馈出错的时间, 在抛错网站上, 有个几分钟前的LuaException, 就是这个Timer的执行流程里抛的......
 于是, 结论: 出错必有LuaException, 出错找不到log, 别忘了灰度测试比例, 尽量在内部测试时放开上报比例.
 
 
PS: 在出错时 执行了timer.remove. 解开了lock, 此时removed = true, running = true. 这种操作, 是否值得商榷?
 
 
 
 

以上是关于ToLua Timer机制的主要内容,如果未能解决你的问题,请参考以下文章

Flink Timer 机制原理,源码整理。

Android--Alarm机制

Egret事件机制

session timer

android中的Timer与Handler的比较

watchdog机制