技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> 算法 --> 记录一个并发引起的 bug

记录一个并发引起的 bug

浏览:1920次  出处信息

    今天发现 Skynet 消息处理的一个 bug ,是由多线程并发引起的。又一次觉得完全把多线程程序写对是件很不容易的事。我这方面经验还是不太够,特记录一下,备日后回顾。


    Skynet 的消息分发是这样做的:

    所有的服务对象叫做 ctx ,是一个 C 结构。每个 ctx 拥有一个唯一的 handle 是一个整数。

    每个 ctx 有一个私有的消息队列 mq ,当一个本地消息产生时,消息内记录的是接收者的 handle ,skynet 利用 handle 查到 ctx ,并把消息压入 ctx 的 mq 。

    ctx 可以被 skynet 清除。为了可以安全的清除,这里对 ctx 做了线程安全的引用计数。每次从 handle 获取对应的 ctx 时,都会对其计数加一,保证不会在操作 ctx 时,没有人释放 ctx 对象。

    skynet 维护了一个全局队列,globalmq ,里面保存了若干 ctx 的 mq 。

    这里为了效率起见(因为有大量的 ctx 大多数时间是没有消息要处理的),mq 为空时,尽量不放在 globalmq 里,防止 cpu 空转。

    Skynet 开启了若干工作线程,不断的从 globalmq 里取出二级 mq 。我们需要保证,一个 ctx 的消息处理不被并发。所以,当一个工作线程从 globalmq 取出一个 mq ,在处理完成前,不会将它压回 globalmq 。

    处理过程就是从 mq 中弹出一个消息,调用 ctx 的回调函数,然后再将 mq 压回 globalmq 。这里不把 mq 中所有消息处理完,是为了公平,不让一个 ctx 占用所有的 cpu 时间。当发现 mq 为空时,则放弃压回操作,节约 cpu 时间。

    所以,产生消息的时刻,就需要执行一个逻辑:如果对应的 mq 不在 globalmq 中,把它置入 globalmq 。

    需要考虑的另一个问题是 ctx 的初始化过程:

    ctx 的初始化流程是可以发送消息出去的(同时也可以接收到消息),但在初始化流程完成前,接收到的消息都必须缓存在 mq 中,不能处理。我用了个小技巧解决这个问题。就是在初始化流程开始前,假装 mq 在 globalmq 中(这是由 mq 中一个标记位决定的)。这样,向它发送消息,并不会把它的 mq 压入 globalmq ,自然也不会被工作线程取到。等初始化流程结束,在强制把 mq 压入 globalmq (无论是否为空)。即使初始化失败也要进行这个操作。


    问题的焦点在于:删除 ctx 不能立刻删除 mq ,这是因为 mq 可能还被 globalmq 引用。而 mq 中并没有记录 ctx 指针(保存 ctx 指针在多线程环境是很容易出问题的,因为你无非保证指针有效),而保存的是 ctx 的 handle 。

    我之前的错误在于,我以为只要把 mq 的删除指责扔给 globalmq 就可以了。当 ctx 销毁的那一刻,检查 mq 是否在 globalmq 中,如果不在,就重压入 globalmq 。等工作线程从 globalmq 中取出 mq ,从其中的 handle 找不到配对的 ctx 后,再将 mq 销毁掉。

    问题就在这里。handle 和 ctx 的绑定关系是在 ctx 模块外部操作的(不然也做不到 ctx 的正确销毁),无法确保从 handle 确认对应的 ctx 无效的同时,ctx 真的已经被销毁了。所以,当工作线程判定 mq 可以销毁时(对应的 handle 无效),ctx 可能还活着(另一个工作线程还持有其引用),持有这个 ctx 的工作线程可能正在它生命的最后一刻,向其发送消息。结果 mq 已经销毁了。

    Skynet 这次在编写过程中,经历过一次大的改变:最早我是采用的一级消息队列,而不是现在的两级。但是一级队列很难同时保证消息的时序性和 ctx 消息处理模块不可被并行运行。在设计修改的过程中,我可能做出了许多不优雅的实现。上面的问题或许可以经过一次梳理,更简单的解决。

    而我现在是这样做的:

    当 ctx 销毁前,由它向其 mq 设入一个清理标记。然后在 globalmq 取出 mq ,发现已经找不到 handle 对应的 ctx 时,先判断是否有清理标记。如果没有,再将 mq 重放进 globalmq ,直到清理标记有效,在销毁 mq 。

建议继续学习:

  1. 一种常见的并发编程场景的处理    (阅读:22596)
  2. Rolling cURL: PHP并发最佳实践    (阅读:10307)
  3. 查看 Apache并发请求数及其TCP连接状态    (阅读:8443)
  4. 大型高并发高负载网站的系统架构分析    (阅读:7715)
  5. 大并发下的高性能编程 – 改进的(用户态)自旋锁    (阅读:7149)
  6. 并发编程系列之一:锁的意义    (阅读:5873)
  7. 并发框架Disruptor译文    (阅读:5081)
  8. 学习:一个并发的Cache    (阅读:4968)
  9. C++多进程并发框架    (阅读:4748)
  10. PHP 持久连接于并发    (阅读:4319)
QQ技术交流群:445447336,欢迎加入!
扫一扫订阅我的微信号:IT技术博客大学习
© 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

京ICP备15002552号-1