深林孤鹰 发表于 2021-7-1 18:51:47

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


@(目录)

# 一、前言

命名这个博客的名称也让我费了点时间,因为之前我在网上找过相关资料,但并没有这方面的资料和讨论,也可能是我太钻牛角尖非要搞个清楚。之前我做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;
            for(int j = 0; j < 1000; j++)
            {
                QCoreApplication::postEvent(this, customEvent + j);
            }
      }
      QMetaObject::invokeMethod(qApp, "quit", Qt::AutoConnection);
    }
```
一次申请1000个Event对象,速度应该会提高些吧?
生气的按下运行,程序直接奔溃。找了半天也不知什么原因。。
# 四、结论
这个结论让我吃惊不小,没想到用锁都比事件传递的效率快,但无需质疑的一点是,因为IOCP建议工作线程要尽快回到系统循环中,所以如果遇到IO或高密集计算,还是得让其他线程来处理。

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

这里没有讨论的一种是原子操作,但理论上应该是比第一种(线程直接操作)慢一点,比第二种(加锁)快。这里就不做讨论了。
页: [1]
查看完整版本: 多线程中任务让谁处理比较高效(Qt为例)