如何正确使用 QThread[转译]

如何正确使用 QThread

  • 一小段历史
    • 很久以前, 继承QThread并重新实现它的run()函数是QThread多线程唯一推荐方法. 它很直观和易用, 但是在工作线程中使用信号槽机制以及Qt事件循环时, 用户常常使用错误. 因此Qt核心开发人员Bradley T. Hughes推荐使用QObject::moveToThread把worker对象移动到线程中. 一些用户开始反对以前的用法, 而实际上, 这两种用法都可以在QThread的文档中找到.

QThread::run()是线程入口点

  • 从Qt的文档中, 我们可以看到如下内容
    A QThread instance represents a thread and provides the means to start() a thread, 
    which will then execute the reimplementation of QThread::run(). The run() 
    implementation is for a thread what the main() entry point is for the application.
    /* 译文: QThread 实例代表一个线程并提供 start() 线程的方法,然后该线程将执行 
    QThread::run() 的重新实现。 run() 实现对于线程就像 main() 入口点对于应用程
    序一样。*/
    
  • 由于 QThread::run() 是线程入口点,因此使用用法 1 是相当直观的。

用法 1-0

  • 要在新线程中运行一些代码,子类化 QThread 并重新实现其 run() 函数。代码如下:
#include <QtCore>

class Thread : public QThread
{
private:
    void run()
    {
        qDebug()<<"From worker thread: "<<currentThreadId();
    }
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    qDebug()<<"From main thread: "<<QThread::currentThreadId();

    Thread t;
    QObject::connect(&t, SIGNAL(finished()), &a, SLOT(quit()));

    t.start();
    return a.exec();
}
  • 输出结果类似下面:
From main thread:  0x15a8 
From worker thread:  0x128c

用法 1-1

  • 由于 QThread::run() 是线程入口点,所以很容易理解,所有在 run() 函数中没有被直接调用的代码都不会在工作线程中执行。
  • 在下面的例子中,成员变量 m_stop 将被 stop() 和 run() 访问。 考虑到前者将在主线程中执行,而后者在工作线程中执行,需要mutex或其他必要的方式。
#if QT_VERSION>=0x050000
#include <QtWidgets>
#else
#include <QtGui>
#endif

class Thread : public QThread
{
    Q_OBJECT

public:
    Thread():m_stop(false)
    {}

public slots:
    void stop()
    {
        qDebug()<<"Thread::stop called from main thread: "<<currentThreadId();
        QMutexLocker locker(&m_mutex);
        m_stop=true;
    }

private:
    QMutex m_mutex;
    bool m_stop;

    void run()
    {
        qDebug()<<"From worker thread: "<<currentThreadId();
        while (1) {
            {
            QMutexLocker locker(&m_mutex);
            if (m_stop) break;
            }
            msleep(10);
        }
    }
};

#include "main.moc"
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    qDebug()<<"From main thread: "<<QThread::currentThreadId();
    QPushButton btn("Stop Thread");
    Thread t;

    QObject::connect(&btn, SIGNAL(clicked()), &t, SLOT(stop()));
    QObject::connect(&t, SIGNAL(finished()), &a, SLOT(quit()));

    t.start();
    btn.show();
    return a.exec();
}
  • 输出结果类似下面这样
From main thread:  0x13a8 
From worker thread:  0xab8 
Thread::stop called from main thread:  0x13a8
  • 可以看到Thread::stop()是在主线程中执行的

用法 1-2(错误用法)

  • 虽然上面的例子很容易理解,但是当在工作线程中引入事件系统(或队列连接)时,它就不那么直观了。
  • 例如,如果我们想在工作线程中定期做一些事情,我们应该怎么做?
    • 在 Thread::run() 中创建一个 QTimer
    • 将超时信号连接到 Thread 的槽
#include <QtCore>

class Thread : public QThread
{
    Q_OBJECT
private slots:
    void onTimeout()
    {
        qDebug()<<"Thread::onTimeout get called from? : "<<QThread::currentThreadId();
    }

private:
    void run()
    {
        qDebug()<<"From worker thread: "<<currentThreadId();
        QTimer timer;
        connect(&timer, SIGNAL(timeout()), this, SLOT(onTimeout()));
        timer.start(1000);

        exec();
    }
};

#include "main.moc"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    qDebug()<<"From main thread: "<<QThread::currentThreadId();

    Thread t;
    t.start();

    return a.exec();
}
  • 乍一看,代码看起来不错。 当线程开始执行时,我们设置一个将在当前线程的事件队列中运行的 QTimer。 我们将 onTimeout() 连接到超时信号。 然后我们看下它是否还在线程中
  • 执行结果如下
From main thread:  0x13a4 
From worker thread:  0x1330 
Thread::onTimeout get called from?:  0x13a4 
Thread::onTimeout get called from?:  0x13a4 
Thread::onTimeout get called from?:  0x13a4
  • 我们惊奇的发现: 它们在主线程而不是工作线程中被调用。
  • 非常有趣(我们将在下一篇博客中讨论这背后发生的事情)
如何解决这个问题呢
  • 为了让这个 SLOT 在工作线程中工作,有人将 Qt::DirectConnection 传递给 connect() 函数,像下面这样:
connect(&timer, SIGNAL(timeout()), this, SLOT(onTimeout()), Qt::DirectConnection);
  • 另一些人试图将下面这行代码添加到线程构造函数中。
moveToThread(this)
  • 它们似乎都按预期工作了。 但是...第二种解决方式明显是错误的.

  • 尽管看起来它跑起来了,但很令人费解,这也不是 QThread 设计的初衷(QThread 中的所有实现函数都是希望在新创建的线程中调用,而不是 QThread 的启动线程)

  • 事实上,根据上面的说法,第一种解决方式也是错误的。 作为 Thread 对象的成员 onTimeout(),也应从创建线程中调用。

  • 两种方式都错了, 我们应该怎么做呢?

用法 1-3

  • 因为 QThread 对象的任何成员都没有设计为从工作线程调用。 所以我们要使用SLOTS就必须创建一个独立的worker对象。
#include <QtCore>

class Worker : public QObject
{
    Q_OBJECT
private slots:
    void onTimeout()
    {
        qDebug()<<"Worker::onTimeout get called from?: "<<QThread::currentThreadId();
    }
};

class Thread : public QThread
{
    Q_OBJECT

private:
    void run()
    {
        qDebug()<<"From work thread: "<<currentThreadId();
        QTimer timer;
        Worker worker;
        connect(&timer, SIGNAL(timeout()), &worker, SLOT(onTimeout()));
        timer.start(1000);

        exec();
    }
};

#include "main.moc"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    qDebug()<<"From main thread: "<<QThread::currentThreadId();

    Thread t;
    t.start();

    return a.exec();
}
  • 执行结果如下:
From main thread:  0x810 
From work thread:  0xfac 
Worker::onTimeout get called from?:  0xfac 
Worker::onTimeout get called from?:  0xfac 
Worker::onTimeout get called from?:  0xfac
  • 问题终于解决了!
  • 虽然这很完美,但是您可能已经注意到,当在工作线程中使用事件循环 QThread::exec() 时,QThread::run() 中的代码似乎与 QThread 本身无关。
  • 那么我们是否可以将对象创建从 QThread::run() 中移出,同时,它们的槽函数仍然会被 QThread::run() 调用?

用法 2-0

  • 如果我们只想使用 QThread::exec(),它默认被 QThread::run() 调用,则不需要再对 QThread 进行子类化。
    • 创建一个 Worker 对象
    • 连接信号与槽
    • 将 Worker 对象移动到子线程
    • start线程
#include <QtCore>

class Worker : public QObject
{
    Q_OBJECT
private slots:
    void onTimeout()
    {
        qDebug()<<"Worker::onTimeout get called from?: "<<QThread::currentThreadId();
    }
};

#include "main.moc"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    qDebug()<<"From main thread: "<<QThread::currentThreadId();

    QThread t;
    QTimer timer;
    Worker worker;

    QObject::connect(&timer, SIGNAL(timeout()), &worker, SLOT(onTimeout()));
    timer.start(1000);

    timer.moveToThread(&t);
    worker.moveToThread(&t);

    t.start();

    return a.exec();
}
  • 结果如下:
From main thread:  0x1310 
Worker::onTimeout get called from?:  0x121c 
Worker::onTimeout get called from?:  0x121c 
Worker::onTimeout get called from?:  0x121c
  • 正如预期的那样,槽函数不在主线程中运行
  • 在这个例子中,QTimer 和 Worker 都被移动到了子线程中。 实际上,不需要将 QTimer 移动到子线程。

用法 2-1

  • 只需删除 timer.moveToThread(&t) 这行; 从上面的例子中也可以按预期工作。
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    qDebug()<<"From main thread: "<<QThread::currentThreadId();

    QThread t;
    QTimer timer;
    Worker worker;

    QObject::connect(&timer, SIGNAL(timeout()), &worker, SLOT(onTimeout()));
    timer.start(1000);

//    timer.moveToThread(&t);
    worker.moveToThread(&t);

    t.start();

    return a.exec();
}
  • 不同之处在于:
    在2-0这个例子中:
    • 信号 timeout() 从子线程发出
    • 由于 timer 和 worker 生活在同一个线程中,因此它们的连接类型是直接连接。
    • 槽函数在发出信号的同一个 thead 中被调用。
      在2-1这个例子中:
    • 信号 timeout() 从主线程发出
    • 由于 timer 和 worker 位于不同的线程中,因此它们的连接类型是排队连接。
    • 槽函数在其活动线程(即子线程)中被调用。
  • 多亏了一种称为queued connections的机制,跨不同线程连接信号和槽是安全的。 如果所有跨线程通信都通过queued connections完成,则不再需要采取QMutex等线程安全措施。

总结

  • 子类化 QThread 并重新实现其 run() 函数很直观,子类化 QThread 仍然有很多完全有效的理由,但是当在工作线程中使用事件循环时,以正确的方式进行操作并不容易。
  • 当事件循环存在时,通过将它们移动到线程来使用工作对象很容易使用,因为它隐藏了事件循环和queued connections的细节。

参考博文

原博文地址

ps: 跨线程的信号和槽连接参数总结

  • Qt::AutoConnection: 如果信号发射和信号接收的对象在同一个线程, 那么默认执行方式为Direct Connection. 否则, 默认执行方式为Queued Connection.
  • Qt::DirectConnection: 信号发射后, 槽立即被调用. 槽函数在信号发送者的线程中执行, 而接收者可能和发送者不在同一个线程.
  • Qt::QueuedConnection: 在控制权返回到接收者线程的事件循环后才调用. 槽函数在接收者的线程中执行. 发送信号后, 槽函数不会立即执行, 而是等接收者的当前函数执行完, 进入事件循环后, 槽函数才会被调用. 多线程环境下, 这个用的最多.
  • Qt::BlockingQueuedConnection: 槽的调用与Queued Connection相同, 但发送者线程会阻塞等待槽函数返回, 如果接收者线程和发送者线程相同, 则不能使用此方式连接, 因为会引起死锁.
  • Qt::UniqueConnection: 单独使用时, 与Auto Connection相同, 但该参数可以防止重复连接, 当重复连接时会连接失败, connect函数返回false. 该参数可以通过按位或(|)与以上四个结合在一起使用。重复连接时会失败, 并返回false.
posted @ 2021-06-08 16:10  技术不支持  阅读(357)  评论(0编辑  收藏  举报