QThread是一个低级(low-level)类,适合用于显式地构建长期运行的线程。
QtConcurrent是一个命名空间,提供了用于编写并发软件的更高层次的类和算法。该命名空间中有一个重要的类,QThreadPool,这是一个管理线程池的类。每个Qt应用程序都有一个QThreadPool::globalInstance()函数,它带有一个推荐的最大线程数,在大多数系统上,处理核的数量就是该值的默认值。
借助于QtConcurrent中函数式的map/filter/reduce算法(它们可将函数并行用到容器中的每一项),通过将进程分布在由线程池管理的多个线程上,可编写一个能够自动利用系统多核的程序。
线程指南
一般情况下,要尽可能避免使用多线程,而是用Qt事件循环与QTimer、非阻塞I/O操作、信号以及短持续时间槽相结合的方法来代替。此外,可以在主线程中长期运行的循环调用QApplication::processEvents(),以使执行工作时图形用户界面可以保持响应。
要驱动动画(animation),建议使用QTimer,QTimeLine或者动画框架(Animation Framework)。这些API并不需要额外创建其它线程。它们允许访问动画代码中的GUI对象而且不会妨碍图形用户界面的响应。
如果要完成CPU密集型工作并希望将其分配给多个处理核,可以把工作分散到QRunnable并通过以下这些推荐做法来实现线程的安全。
1)无论何时,都尽可能使用QtConcurrent算法把CPU密集型计算工作分散给多线程,而不是自己编写QThread代码。
2)除了主线程以外,不要从其它任何线程访问图形用户界面(这也包括那些由QWidget派生的类、QPixmap和那些与显卡相关的类)。这包括读取操作,比如查询QLineEdit中输入的文本。
3)要其他线程中处理图像,使用QImage而不是QPixmap。
4)不要调用QDialog::exec()或者从除主线程之外的任何线程创建QWidget或QIODevice的子类。
5)使用QMutex、QReadWriteLock或者QSemaphone以禁止多个线程同时访问临界变量。
6)在一个拥有多个return语句的函数中使用QMutexLocker(或者QReadLocker、QWriteLocker),以确保函数从任意可能的执行路径均可释放锁。
7)创建QObject的线程,也称线程关联(thread affinity),负责执行那个QObject的槽。
8)如果各QObject具有不同的线程关联,那么就不能以父—子关系来连接它们。
9)通过从run()函数直接或者间接调用QThread::exec(),可以让线程进入事件循环。
10)利用QApplication::postEvent()分发事件,或使用队列式的信号/槽连接,都是用于线程间通信的安全机制——但需要接收线程处于事件循环中。
11)确保每个跨线程连接的参数类型都用qRegisterMetaType()注册过。
线程安全和QObject
可重入(reentrant)函数就是一个可以由多个线程同时调用的函数,其中任意的两次调用都不会试图访问相同的数据。线程安全的方法在任何时间都可以同时由多个线程调用,因为任何共享数据都会在某种程度上(例如,通过QMuex)避免被同时访问。如果一个类的所有非静态函数都是可重入的或者是线程安全的,那么它就是可重入的或者是线程安全的。
一个QObject在它所”属于“或者有关联的线程中被创建。其各子对象也必须属于同一线程。Qt禁止跨线程的父——子关系。
1)QObject::thread()可返回它的所有者线程,或者是其关联线程。
2)QObject::moveToThread()可将其移动到另一个线程
moveToThread(this)
由于QThread是一个QObject而且在需要额外的线程时才会创建QThread,因此,即使你会认为QThread和线程是可以相互指代的,也是可以理解的。尽管如此,那个额外的线程在调用QThread::start()之前实际上都不会被创建,这使得问题更难于理解。
回想一下,每个QThread的本质都是一个QObject,这决定了它与其创建的线程存在关联,而不是与它启动的线程存在关联。
正是因为这个原因,有人说QThread并不是线程本身,而是该线程的管理器。这或许也可以有助于理解这一方式。实际上,QThread是一个底层线程API的封装器,也是一个基于java.lang.thread API的管理单个线程的管理器。
这就意味着,当信号连接到这个QThread的槽上时,槽函数的执行是在其创建线程,而不是在其管理的线程进行的。
一些程序通过改变QThread的定义使它可表示其管理的线程并在该线程内执行执行它的槽。这些程序使用一种变通方法:在QThread的构造函数中使用moveToThread(this)。这一变通方法的主要问题是,在线程退出后,通过post方式派发给该对象的事件如何处理留下不确定性。
线程安全的对象就是一个可以由多个线程同时访问并且可确保处于”有效“状态的对象。默认情况下,QObject不是线程安全的。为了让一个对象线程安全,可以利用以下方法。
1)QMutex用于保证互斥,可与QMutexLocker一起使用,它允许一个单独的线程T保护(锁住)一个对象或者一段代码,使其在线程T释放(解锁)之前不能被其它的线程访问。
2)QWaitCondition与QMutex结合使用,可以把某个线程置于一种不忙的阻塞状态,这种状态下,可让其等待另外一个线程将其唤醒。
3)QSemaphore是一个广义的QMutex,可以用在一个线程在开始工作之前需要锁住不止一个资源的各种情况。信号量使其能够保证线程仅在要进行工作所需的资源全部满足的情况下才锁住资源。
moveToThread的使用示例
class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork(const QString ¶meter) {
QString result;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
}
signals:
void resultReady(const QString &result);
};
class Controller : public QObject
{
Q_OBJECT
QThread workerThread;
public:
Controller() {
Worker *worker = new Worker;
worker->moveToThread(&workerThread);
connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
connect(this, &Controller::operate, worker, &Worker::doWork);
connect(worker, &Worker::resultReady, this, &Controller::handleResults);
workerThread.start();
}
~Controller() {
workerThread.quit();
workerThread.wait();
}
public slots:
void handleResults(const QString &);
signals:
void operate(const QString &);
};
在Worker槽函数内部的代码将会在单独的线程内执行。
另外一种让代码在单独的线程内运行的方法是子类化QThread并重新实现run()函数。比如:
class WorkerThread : public QThread
{
Q_OBJECT
void run() Q_DECL_OVERRIDE {
QString result;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
}
signals:
void resultReady(const QString &s);
};
void MyObject::startWorkInAThread()
{
WorkerThread *workerThread = new WorkerThread(this);
connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
workerThread->start();
}
在示例中,线程会在run()函数返回后退出。除非你调用exec()函数,否则不会有任何事件循环运行在线程内。
重要的是记住一个QThread实例驻留在实例化它的旧线程中,而不是在调用run()函数的新线程中。这意味着所有QThread的排队槽函数都会旧线程内执行。因此,想要在新线程中调用槽函数的开发者必须使用worker对象的方法;新的槽函数不应该被直接实现成子类化的QThread。
当子类化QThread时,记住构造函数是在旧线程中执行的,而run()函数是在新线程中执行的。如果一个成员变量被两个函数访问,那么变量是被两个不同的线程访问。这时得检查这样做是否安全。
在次线程中使用Qt的类
当函数可以同时被不同的线程安全地调用时,就称其为”线程安全的“(thread-safe)。如果在不同的线程中对某一共享数据同时调用两个线程安全的函数,那么结果将总是可以确定的。若将这个概念推广,当一个类的所有函数都可以同时被不同的线程调用,并且它们之间互不干涉,即使是在操作同一个对象的时候也互不妨碍,我们就把这个类称为是”线程安全的“。
一个类是否是可重入的,在Qt的参考文档中有标记。通常情况下,任何没有被全局引用或者被其他共享数据引用的C++类都认为是可重入的。
QObject是可重入的,但有必要记住它的三个约束条件:
1、QObject的子对象必须在它的父对象线程中创建
特别需要说明的是,这一约束条件意味着在次线程中创建的对象永远不能将QThread对象作为创建它们的父对象,因为QThread对象是在另外一个线程(主线程或者·另外一个不同的次线程)中创建的。
2、在删除对应的QThread对象之前,必须删除所有在次线程中创建的QObject对象
通过在QThread::run()中的堆栈上创建这些对象,就可以完成这一点。
3、必须在创建QObject对象的线程中删除它们
如果需要删除一个存在于不同线程中的QObject对象,则必须调用线程安全的QObject::deleteLater()函数,它可以置入”延期删除“(deferred delete)事件。
由于从那些为Qt的图形用户界面支持提供编译的低级库上继承的局限性,QWidget和它的子类都是不可重入的。这样造成的后果之一就是我们不能在一个来自次线程的窗口部件上直接调用函数。打个比方说,如果想从次线程中修改一个QLabel的文本,则可以发射一个连接到QLabel::setText()的信号,或者从该线程中调用QMetaObject::invokeMethod()。例如:
void MyThread::run()
{
QMetaObject::invokeMethod(label, SLOT(setText(const QString&)), Q_ARG(QString, "Hello"));
}