• 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. 这种操作, 是否值得商榷?
     
     
     
     
  • 相关阅读:
    Super
    多态
    方法覆盖 和toString方法的作用
    Static 静态+this
    构造方法 + 继承
    使用方法重载的优缺点
    Erlang 简介与样例分析
    Assassin's Creed 4: Black Flag --《刺客信条4; 黑旗》
    DEVIL MAY CRY V:《鬼泣5》
    Valiant Hearts: The Great War -- 《勇敢的心》
  • 原文地址:https://www.cnblogs.com/wmalloc/p/11245423.html
Copyright © 2020-2023  润新知