记录一个并发引起的 bug
今天发现 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 。
建议继续学习:
- 一种常见的并发编程场景的处理 (阅读:22628)
- Rolling cURL: PHP并发最佳实践 (阅读:10358)
- 查看 Apache并发请求数及其TCP连接状态 (阅读:8501)
- 大型高并发高负载网站的系统架构分析 (阅读:7737)
- 大并发下的高性能编程 – 改进的(用户态)自旋锁 (阅读:7174)
- 并发编程系列之一:锁的意义 (阅读:5919)
- 并发框架Disruptor译文 (阅读:5130)
- 学习:一个并发的Cache (阅读:4995)
- C++多进程并发框架 (阅读:4771)
- PHP 持久连接于并发 (阅读:4340)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:云风的 BLOG 来源: 云风的 BLOG
- 标签: Skynet 并发
- 发布时间:2012-08-20 23:38:51
- [46] 界面设计速成
- [42] Oracle MTS模式下 进程地址与会话信
- [41] 视觉调整-设计师 vs. 逻辑
- [40] IOS安全–浅谈关于IOS加固的几种方法
- [39] android 开发入门
- [38] 如何拿下简短的域名
- [38] 【社会化设计】自我(self)部分――欢迎区
- [38] 图书馆的世界纪录
- [37] 程序员技术练级攻略
- [34] 读书笔记-壹百度:百度十年千倍的29条法则