记一次Bug 调试 —— 多线程

前言

Pistache,不愧是一个开源软件,其组件真的存在各种问题,今天又被其PollableQueue给坑了,之前修复了其中存在的一个内存泄漏Bug,见《记一次Bug 调试 —— 内存泄漏&&内存越界》。今天又发现了一个关于多线程的Bug。

PollableQueue是基于父类Pistache::Queue实现的,Pistache::Queue是一种无锁的MPSC设计,即多生产者单消费者模型。因此,其设计已经保证了多线程安全,而然PollableQueue为了实现 Pollable 属性,添加了一个eventfd,从而为多线程埋下了隐患。

关于eventfd可以参考我翻译的linux手册《linux手册翻译——eventfd(2)》,手册告诉我们,eventfd也是线程安全的,因此PollableQueue在大部分场景下都没有太大问题,但是偏偏被我遇到了有问题的场景。

源代码

#include <pistache/mailbox.h>
#include <thread>
#include <atomic>

using namespace Pistache;
using namespace std;

int main() {
    Polling::Epoll poller;
    PollableQueue<int> queue;
    queue.bind(poller);
    atomic<int> count = 0;
    thread t([&] {
        for (;;) {
            vector<Pistache::Polling::Event> events;
            int ready_fds = poller.poll(events);
            for (const auto &e:events)
                if (e.tag == queue.tag())
                    for (;;) {
                        auto t = queue.popSafe();
                        if (!t)
                            break;
                        printf("%d ", *t);
                        count--;
                    }

        }
    });
    sleep(1);
    for (int i = 0;;) {
        if (count == 0) {
            count++;
            queue.push(i++);
        }
    }
}
代码逻辑

逻辑非常简单,就一个生产者,一个消费者,并且使用了一个atomic<int> count = 0;用于描述队列的最大容量,在这里最大容量为1。即生产者产生一个数据,只有在消费者消费之后,才会继续生产。
理论上来说,这个模型没有什么线程安全问题,程序会一直运行。

运行结果

程序运行一段时间就会停止(停止的位置不确定),查看函数栈会发现,消费者者停留在poller.poll(events),且此时count的值为1,生产者一直空循环。

也就是说,生产者产生了数据(因为count值为1),但是消费者却没有感知到,因为消费者是通过eventfd感知数据到达的,为什么生产者执行了push操作,消费者却感知不到呢?

源码分析

我们需要查看pop的源码(稍微i简化):

Entry* pop() override
        {
            auto ret = Queue<T>::pop();
            if (isBound())
            {
                uint64_t val;
                ssize_t bytes = read(event_fd, &val, sizeof val);
            }
            return ret;
        }

我们可以看到,每次执行pop,无论是否有结果返回,都会清空eventfd,而问题正是出现在这里:
如果在执行了auto ret = Queue<T>::pop();之后,发现ret为空,但是此时刚好有新数据到达,那么这个时候清空eventfd,就会导致新数据的eventfd也被清除,从而导致epoll无法检测到新的数据到达了
我们结合我们自己的测试用例分析上述过程:

  1. 生产者判断count为0,使count++,并执行push放入数据,此后在count被消费者置为0之前都不会继续push
  2. 消费者的epoll检测到eventfd事件,报告queue中产生了新的数据
  3. 执行以下循环处理数据:
        for (;;) {
         auto t = queue.popSafe();
         if (!t)
             break;
         printf("%d ", *t);
         count--;
       } 
    
  4. 第一次循环,消费了生产者的数据,count--,此时生产者已经具备了可执行的条件
  5. 然后消费者再次执行pop操作,当执行完auto ret = Queue<T>::pop();后生产者被调度,产生了数据并将count++,注意此时ret的值是null
  6. 消费者继续执行,清除eventfd,在这里将抹除生产者刚刚产生的eventfd记录
  7. 消费因为pop返回null导致for循环推出
  8. 消费者,继续执行int ready_fds = poller.poll(events);等待事件到达,但是我们在第6步清除了eventfd,导致此时无法检测到新数据到达
  9. 如果此时生产者还能继续产生消息,那么这个问题将被解决,但是生产者此时由于消费者没有将count--,也无法产生数据,从而导致程序停止输出!!!

Bug解决

解决方案也很简单:

Entry* pop() override
        {
            auto ret = Queue<T>::pop();
            if (isBound() && ret != nullptr)
            // if (isBound())
            {
                uint64_t val;
                ssize_t bytes = read(event_fd, &val, sizeof val);
            }
            return ret;
        }

加一个判断即可,如果ret返回是null,那么就不要再清除eventfd了!

思考

其实如果生产者可以源源不断的产生数据,那么其实程序就不会产生这种类似于死锁的问题,在Pistache中使用PollableQueue的确不会像我这样限制queue的大小,因此不会导致严重的后果,但是也会导致请求无法被及时处理,因此这个改进是应该的!

标题给的是多线程,但这好像不是严格意义上的多线程访问共享资源导致的安全问题,反而更类似与死锁问题!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容