找回密码
 立即注册
收起左侧

多线程中任务让谁处理比较高效(Qt为例)

0
回复
15632
查看
[复制链接]
累计签到:7 天
连续签到:1 天
来源: 原创 2021-7-1 18:51:47 显示全部楼层 |阅读模式

马上注册,查看详细内容!注册请先查看:注册须知

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

@TOC

一、前言

命名这个博客的名称也让我费了点时间,因为之前我在网上找过相关资料,但并没有这方面的资料和讨论,也可能是我太钻牛角尖非要搞个清楚。之前我做IOCP库的时候,想把这个高并发网络库做的更精益求精,用了很多晦涩的逻辑和技术来实现无锁化,但这只是建立在我一直认为发送事件的效率比锁的速度要快,今天终于有时间来验证一下这个想法,但结果却让我大跌眼镜。当然也有可能是我的思路或者方法有问题,希望有大神了解这方面的可以指点指教。

二、验证目的

因为IOCP的实现中,会开多个工作线程来接受数据和处理任务,也会遇到处理临界区资源的情况,我的无锁化实现实质就是将这些数据都Post给主线程,让主线程来处理所有的任务,这样的好处就是让工作线程尽快回到队列中取数据,且主线程处理任务就不用加锁,运行效率会更高(我自认为的)。现在为了验证到底是工作线程加锁来处理任务、还是工作线程将任务Post给主线程的效率高。

三、验证流程

为了简化程序,我只用了一个工作线程和主线程来完成验证,任务很简单,就是给一个全局变量加1,直到给定的一个最大值。

1、工作线程直接无锁处理

这个是最快速的,让我们先看看最快的速度是多少。
相关代码:

#define COUNT 5000000

    void run()
    {
        while(count <= COUNT)
        {
            count++;
        }
        qApp->exit();
    }

测试结果为15ms左右。

2、工作线程加锁处理

假设变量count是个临界区,对它修改时要加锁。
相关代码:

#define COUNT 5000000

    void run()
    {
        while(1)
        {
            mutex.lock();
            if(count <= COUNT)
            {
                count++;
                mutex.unlock();
            }
            else
            {
                mutex.unlock();
                break;
            }
            //sleep(0);
        }
        //qDebug() << "3:" << Worker::time.elapsed();
        qApp->exit();
    }

测试结果为100ms左右。

3、使用信号槽发送任务

开始我认为效率最高的处理办法吧。
相关代码:

signals:
    void s_opt();
    //。。。
    connect(this, SIGNAL(s_opt()), this, SLOT(opt()), Qt::AutoConnection);
    //。。。
    Q_INVOKABLE void opt() {
        count++;
    }
    //。。。
    void run()
    {
        for(int i = 0; i <= COUNT; i++)
        {
            emit s_opt();
        }
        QMetaObject::invokeMethod(qApp, "quit", Qt::AutoConnection);
    }

满怀信心的点了运行,然后闷逼的等了一会,给我显示个出来:7709ms
思来想去,是不是我点运行的姿势不对?多点了几次,也就这个值左右。
我感觉不可思议,但也不死心,把代码改了一下:


    void run()
    {
        for(int i = 0; i <= COUNT; i++)
        {
            QMetaObject::invokeMethod(pobj, "opt", Qt::AutoConnection); 
        } 
        QMetaObject::invokeMethod(qApp, "quit", Qt::AutoConnection);
    }

但还是试试吧。点了运行后,结果果然差不多。
还不死心,继续改了一下,将AutoConnection改为QueuedConnection,继续测试!
没想到结果是:29070ms
我有点蒙逼了,两个问题:1是用信号槽或invokeMethod不至于慢上几十倍吧?2是AutoConnection在跨线程不是默认QueuedConnection吗?

4、使用postEvent

思来想去,再试试发送事件的办法吧,毕竟信号槽在跨线程的实现上也是基于这个的。
相关代码:

    void run()
    { 
        for(int i = 0; i <= COUNT; i++)
        {
            CustomEvent *customEvent = new CustomEvent;
            QCoreApplication::postEvent(this, customEvent); 
        } 
        QMetaObject::invokeMethod(qApp, "quit", Qt::AutoConnection);
    }
    bool event(QEvent *event)
    {
        if (event->type() == CustomEvent::eventType())
        {
            count++; 
            return true;
        }
        return Worker::event(event);
    }

小心翼翼的按下运行,结果显示为:29002ms
果然问题在这里,难道是堆上建立Event对象耗费时间吗?那把代码稍微再改改:

    void run()
    { 
        for(int i = 0; i <= COUNT / 1000; i++)
        {
            CustomEvent *customEvent = new CustomEvent[1000];
            for(int j = 0; j < 1000; j++)
            {
                QCoreApplication::postEvent(this, customEvent + j);
            } 
        } 
        QMetaObject::invokeMethod(qApp, "quit", Qt::AutoConnection);
    }

一次申请1000个Event对象,速度应该会提高些吧?
生气的按下运行,程序直接奔溃。找了半天也不知什么原因。。

四、结论

这个结论让我吃惊不小,没想到用锁都比事件传递的效率快,但无需质疑的一点是,因为IOCP建议工作线程要尽快回到系统循环中,所以如果遇到IO或高密集计算,还是得让其他线程来处理。

至于为什么发送事件会如此耗时,个人认为除了新建、删除事件对象会耗时外,postEvent函数是由于线程安全的,所以本身应该也有一些耗时处理,加上唤醒主线程这些额外的损耗,综合这些原因才导致慢了这么多。

这里没有讨论的一种是原子操作,但理论上应该是比第一种(线程直接操作)慢一点,比第二种(加锁)快。这里就不做讨论了。

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了