• 1579阅读
  • 6回复

[讨论]多线程中任务让谁处理比较高效(Qt为例) [复制链接]

上一主题 下一主题
离线leamus
 

只看楼主 倒序阅读 楼主  发表于: 2021-07-01
@[TOC](目录)

# 一、前言

命名这个博客的名称也让我费了点时间,因为之前我在网上找过相关资料,但并没有这方面的资料和讨论,也可能是我太钻牛角尖非要搞个清楚。之前我做IOCP库的时候,想把这个高并发网络库做的更精益求精,用了很多晦涩的逻辑和技术来实现无锁化,但这只是建立在我一直认为发送事件的效率比锁的速度要快,今天终于有时间来验证一下这个想法,但结果却让我大跌眼镜。当然也有可能是我的思路或者方法有问题,希望有大神了解这方面的可以指点指教。
# 二、验证目的
因为IOCP的实现中,会开多个工作线程来接受数据和处理任务,也会遇到处理临界区资源的情况,我的无锁化实现实质就是将这些数据都Post给主线程,让主线程来处理所有的任务,这样的好处就是让工作线程尽快回到队列中取数据,且主线程处理任务就不用加锁,运行效率会更高(我自认为的)。现在为了验证到底是工作线程加锁来处理任务、还是工作线程将任务Post给主线程的效率高。

# 三、验证流程
为了简化程序,我只用了一个工作线程和主线程来完成验证,任务很简单,就是给一个全局变量加1,直到给定的一个最大值。
## 1、工作线程直接无锁处理
这个是最快速的,让我们先看看最快的速度是多少。
相关代码:

```cpp
#define COUNT 5000000

    void run()
    {
        while(count <= COUNT)
        {
            count++;
        }
        qApp->exit();
    }
```
测试结果为**15ms**左右。
## 2、工作线程加锁处理
假设变量count是个临界区,对它修改时要加锁。
相关代码:

```cpp
#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、使用信号槽发送任务
开始我认为效率最高的处理办法吧。
相关代码:

```cpp
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**。
思来想去,是不是我点运行的姿势不对?多点了几次,也就这个值左右。
我感觉不可思议,但也不死心,把代码改了一下:

```cpp

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

```cpp
    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对象耗费时间吗?那把代码稍微再改改:

```cpp
    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函数是由于线程安全的,所以本身应该也有一些耗时处理,加上唤醒主线程这些额外的损耗,综合这些原因才导致慢了这么多。

这里没有讨论的一种是原子操作,但理论上应该是比第一种(线程直接操作)慢一点,比第二种(加锁)快。这里就不做讨论了。
4条评分好评度+1贡献值+1金钱+10威望+1
20091001753 好评度 +1 - 2021-07-02
20091001753 贡献值 +1 - 2021-07-02
20091001753 威望 +1 - 2021-07-02
20091001753 金钱 +10 - 2021-07-02
离线snow_man_0

只看该作者 1楼 发表于: 2021-07-01
我觉得线程运行独立,尽量不要让事件干扰
离线leamus

只看该作者 2楼 发表于: 2021-07-01
回 snow_man_0 的帖子
snow_man_0:我觉得线程运行独立,尽量不要让事件干扰 (2021-07-01 19:03) 

要分情况,一般线程可以处理特定的一个任务,运行完毕后退出,但有些线程可以开启事件循环来等待事件,比如主要处理IO的线程就等待IO事件。
离线kaon

只看该作者 3楼 发表于: 2021-07-02
应该比较调用5000000次函数和调用5000000次信号槽的时间吧
离线leamus

只看该作者 4楼 发表于: 2021-07-02
回 kaon 的帖子
kaon:应该比较调用5000000次函数和调用5000000次信号槽的时间吧
 (2021-07-02 09:44) 

很明显调用函数要比调用信号槽快得多,函数调用基本都可以忽略了。而且这两个比较没什么意义,前者只是代码执行跳转,后者是一种设计模型。
离线kaon

只看该作者 5楼 发表于: 2021-07-02
我这边测试没你这么夸张,基本上用sigslot比mutex慢3倍这样
离线leamus

只看该作者 6楼 发表于: 2021-07-03
回 kaon 的帖子
kaon:我这边测试没你这么夸张,基本上用sigslot比mutex慢3倍这样 (2021-07-02 20:33) 

信号槽的连接方式是什么?auto还是queue?auto的话看是否在一个线程内,如果在一个线程内,那就相当于直接调用槽函数,效率也就很高。我测试的基本都是以queue方式连接的,也就是使用postEvent。
快速回复
限100 字节
 
上一个 下一个