本文翻译了4篇(段)关于QT线程(QThread)的英文文档,链接如下:
http://doc.qt.io/qt-5/qthread.html#details
http://doc.qt.io/qt-5/qthread.html#managing-threads
http://doc.qt.io/qt-5/qobject.html#thread-affinity
http://doc.qt.io/qt-5/threads-synchronizing.html
译文如下:
QThread之Details
一个QThread的对象管理一个线程。QThreads在run()函数中开始运行。在线程中run()函数通过调用exec()函数启动event loop并运行。
可以通过QObject::moveToThread()将worker对象移动到工作线程中来使用它们。
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的slot中的代码将在一个单独的线程中运行。但是,你可以将worker的slots和任何线程的任何对象的任何signal进行连接。连接不同线程中的signal和slot是安全的,这是由于"queued connections"机制。(注:该连接类型指的是,一旦当CPU的控制权回到接收者线程的event loop后就会调用slot函数)
还有一种方法可以使代码运行在另一个线程中。这就是继承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()函数结束运行后退出。在此线程中不会有event loop,除非调用了exec().
注意,一个QThread实例是存在于(lives in)老线程(即初始化该实例的线程)中的,而不是存在于新的调用run()的线程中的。这就是说,所有QThread的queued slots都将在老线程中被执行。因此,如果希望在新线程中调用slots函数,就必须使用worker-object的方法(译注:应该指上面第二种方法);新slots函数不应该在QThread子类中直接实现。
当继承QThread类时,记住,构造函数在老线程中执行,而run()在新线程中执行。如果一个成员变量被2个函数都访问,那么这个变量就是被2个不同的线程在访问。要检查这么做是否安全。
附:
int QThread::exec()
进入event loop并等待直到exit()被调用,返回传递给exit()的值。如果exit()是通过quit()被调用的,那么该返回值就是0.
这个方法一般是在run()函数中被调用的。启动event handling就必须要调用此方法。
管理线程
当线程是started()或finished(),QThread会通过一个signal来通知你,或者你可以使用isFinished()和isRunning()来检查线程的状态。
可以通过调用exit()和quit()来结束线程。在极端情况下,可以使用terminate()强制结束一个正在执行的线程。但是,这么做是危险的和不推荐的。请阅读关于terminate()和setTerminationEnabled()的文档。
Qt4.8及以后,通过连接finished() signal和QObject::deleteLater(),可以释放在一个线程中刚刚结束的对象。
使用wait()来阻塞正在被调用的线程,直到其他线程结束了执行(或特定时间超时)。
QThread也提供静态的,平台无关的sleep函数,如sleep(), msleep(), 和usleep()来允许秒级,毫秒级,微秒级的休眠。这些函数直到Qt5.0才成为public的。
注意,wait()和sleep()函数一般应该是不必要的,因为Qt是一个事件驱动的框架。可以考虑以监听finished() signal来代替wait(),而使用QTimer来代替sleep()函数。
静态函数currentThreadId()和currentThread()返回当前线程的Id. 前者返回一个平台相关的ID,而后者返回一个QThread指针。
为了选择你的线程的名字(如同在Linux上运行"ps -L"命令所显示的),你可以在启动线程之前调用setObjectName(). 如果不调用setObjectName(),线程的名字将是你的线程对象运行时类型的类名(比如,对于Manderbrot Example,"RenderThread"就是线程名)。注意,在Windows系统上,这个是目前不支持的。
线程关联(Thread Affinity)
一个QObject对象是有线程关联性的,或者说它是存在于某特定线程中的。当一个QObject对象接收到一个queued signal或者一个被发送来的event时,对应的slot函数或event handler就会在该对象所在的线程中运行。
注意: 如果一个QObject对象没有线程关联性(也就是说,如果thread()函数返回0),或者它所存在的线程没有event loop,那么它就无法接收queued signal或posted events.
默认情况下,QObject对象存在于它被创建的那个线程中。可以使用thread()函数来查询一个对象的线程关联性,而用moveToThread()来改变一个对象的线程关联。
所有QObjects都必须存在于和它们的父亲所在的线程中。因此:
- 如果2个QObject对象存在于不同的线程中,那么setParent()调用就会失败;
- 当一个QObject对象被移到另一个线程中,它所有的孩子也会被自动移过去;
- 如果一个QObject对象有一个父线程,那么对该对象调用moveToThread()会失败;
- 如果QObject对象被创建在QThread::run()中,它们就无法成为那个QThread对象的孩子,因为那个QThread对象并不存在于调用QThread::run()的线程中。
线程同步
为了同步线程,Qt提供了底层的原语和上层的机制。底层同步原语
QMutex是强制互斥的基础类。QReadWriteLock和QMutex类似,不同的是它会区分读访问和写访问。当一段数据并没有被写的时候,多个线程同时去读取它是安全的。而QMutex在此种情况下仍会强制多个读线程串行去读。因为QReadWriteLock允许同时读,所以它更高效。
QSemaphore是对QMutex的泛化。QMutex只保护一个资源,而QSemaphore保护一定数量的资源。
QWaitCondition并不通过强制互斥而是通过条件变量来实现线程同步。QWaitCondition使得线程一直等待,直到一个特定条件满足。为了允许在waiting的线程继续工作,调用wakeOne()可以随机唤醒一个正在等待的线程;而wakeAll()则将所有等待的线程都唤醒。Wait Conditions Example给出了如何通过QWaitCondition来解决生产者消费者问题。
注意:QT的线程同步类依赖于正确对齐的指针的使用。比如,用MSVC你就无法使用紧凑型的类(packed classes)。
这些同步类可以使得一个方法线程安全。但是,这么做也会影响性能,这就是为什么大多数Qt的方法并不是线程安全的。
风险
如果一个线程锁住了一个资源而没有释放它,那么整个应用就会冻结住,因为所有其他线程都永远无法获得该资源。这是可能的,比如,如果抛出了一个异常并迫使当前函数没有释放锁就返回了。另一个类似的场景是死锁。比如,假设2个线程各自等待对方所持有的一个资源,那么就会陷入死锁,而应用就会冻结住。
方便使用的类
QMutexLocker、QReadLocker和QWriteLocker是方便的类。利用它们可以更加容易地使用QMutex和QReadWriteLock. 它们在构造的时候锁住一个资源,而当它们销毁的时候会自动释放资源。它们被设计用来简化使用QMutex和QReadWriteLock的代码,因此减少了使得一个资源被永远锁住的可能。高层事件队列
Qt的event system对于线程间通信非常有用。每个线程都可能有它自己的event loop. 为了在另一个线程中调用一个slot(或任何可以被调用的方法),把调用放在目标线程的event loop中。这使得目标线程在完成它当前的任务后就去执行slot函数,而原来的线程将继续并行地运行。
为了在一个event loop中进行调用,要进行queued signal-slot连接。无论该signal在何时被发射,它的参数将被event system记录下来。接收者所在的线程将运行该slot函数。或者,调用QMetaObject::invokeMethod()来达到同样的效果而无需使用signals. 在这2种情况下,一个queued connection必须被使用,因为一个direct connection绕过了event system,使得该方法在当前线程中被立即执行。
和底层原语不同,当使用event system来进行线程同步的时候,没有发生死锁的风险。但是event system并没有进行互斥。如果被调用的方法访问共享数据的时候,共享数据的访问必须被底层原语保护。
正如之前所说,没有隐式共享(implicitly shared)的Qt的event system,提供了一种和传统的线程同步不同的机制。如果只是signals和slots被调用了,而在线程间没有共享的变量,一个多线程的程序就可以不需要底层原语了。