Threads are about doing things in parallel, just like processes. So how do threads differ from processes? While you are making calculations on a spreadsheet, there may also be a media player running on the same desktop playing your favorite song. Here is an example of two processes working in parallel: one running the spreadsheet program; one running a media player. Multitasking is a well known term for this. A closer look at the media player reveals that there are again things going on in parallel within one single process. While the media player is sending music to the audio driver, the user interface with all its bells and whistles is being constantly updated. This is what threads are for — concurrency within one single process.
那么,如何实现并发呢?在单核 CPU 上并行工作是一种错觉,有点类似于电影中移动图像的错觉。对于进程,错觉是在很短时间后中断处理器在一进程中的工作而产生的。然后,处理器继续处理下一进程。为在进程之间切换,保存当前程序计数器,并加载下一处理器的程序计数器。这还不够,因为需要对寄存器、某些体系结构及特定 OS 数据执行相同操作。
就像一个 CPU 可以驱动 2 个或多个进程,也可以让 CPU 运行在一个单进程的 2 个不同代码段中。当进程启动时,它始终执行一代码段,因此说进程拥有一线程。不管怎样,程序可能决定启动第 2 线程。然后,在一个进程内同时处理 2 个不同的代码序列。通过重复保存程序计数器和寄存器,然后加载下一线程的程序计数器和寄存器,在单核 CPU 中达成并发。在活动线程之间循环,不需要程序的合作。当切换到下一线程出现时,线程可能处于任何状态。
CPU 设计的当前趋势是拥有多个核心。典型的单线程应用程序只能使用一个核心。不管怎样,可以将具有多个线程的程序赋值给多个核心,从而使事情以真正的并发方式发生。结果,将工作分发给多个线程可以使程序在多核 CPU 上运行得更快,因为可以使用其它核心。
如前所述,每个程序拥有一线程当它启动时。该线程被称为主线程 (在 Qt 应用程序中又称为 GUI 线程)。Qt GUI 必须在此线程中运行。所有 Widget 和几个相关类,例如 QPixmap ,不工作于第 2 线程。第 2 线程通常称为工作者线程,因为它用于从主线程分担处理工作。
每个线程拥有自己的堆栈,意味着每个线程拥有自己的调用历史和局部变量。不像进程,线程共享相同地址空间。以下简图展示如何在内存中定位线程构造块。非活动线程的程序计数器和寄存器通常保持在内核空间中。有共享代码副本,且每个线程有单独堆栈。
若 2 线程拥有相同对象指针,则 2 线程同时访问该对象是可能的,且这可能潜在破坏对象的完整性。很容易想象很多事情可能出错,当同一对象的 2 方法同时执行时。
有时有必要从不同线程访问某一对象;例如,当活在不同线程中的对象需要通信时。由于线程使用相同地址空间,线程交换数据更容易且更快,相比进程。不必序列化和拷贝数据。传递指针是可能的,但必须严格协调什么线程接触哪个对象。必须防止在一对象上同时执行操作。有几种办法能达成这且下文将描述其中一些办法。
那么,怎样做才安全呢?可以安全地使用在线程中创建的所有对象在该线程中,前提是其它线程没有它的引用且对象没有隐式耦合其它线程。这种隐式耦合可能发生,当采用静态成员、单例或全局数据在实例之间共享数据时。熟悉的概念是 线程安全和可重入 类和函数。
基本上,线程有 2 种使用案例:
开发者采用线程时需要很小心。启动其它线程很容易,但很难确保所有共享数据仍然一致。问题经常难以发现,因为它们可能仅偶尔出现一次,或仅在特定硬件配置上出现。在创建线程解决某些问题前,应考虑可能的替代。
| Alternative | 注释 |
|---|---|
| QEventLoop::processEvents () | 调用 QEventLoop::processEvents () 重复在耗时计算期间防止 GUI 阻塞。然而,此解决方案伸缩性不好,因为调用 processEvents() 发生次数可能过多或不足,从属硬件。 |
| QTimer | 有时可以使用计时器方便履行后台处理,以在将来某个时间点调度槽的执行。0 间隔计时器将尽快超时,一旦没有更多要处理的事件。 |
| QSocketNotifier QNetworkAccessManager QIODevice::readyRead () | 这是拥有一个或多个线程的替代,每个在缓慢网络连接上阻塞读取。只要可以快速执行响应网络数据组块的计算,这种反应式设计就比等待线程同步更优。反应式设计比线程更不易于出错且高效节能。在许多情况下,还有性能好处。 |
In general, it is recommended to only use safe and tested paths and to avoid introducing ad-hoc threading concepts. QtConcurrent provides an easy interface for distributing work to all of the processor's cores. The threading code is completely hidden in the QtConcurrent 框架,因此,不必关心细节。不管怎样, QtConcurrent 不可以使用当需要与正运行线程通信时,且不应将其用于处理阻塞操作。
Sometimes you want to do more than just running a method in the context of another thread. You may want to have an object which lives in another thread that provides a service to the GUI thread. Maybe you want another thread to stay alive forever to poll hardware ports and send a signal to the GUI thread when something noteworthy has happened. Qt provides different solutions for developing threaded applications. The right solution depends on the purpose of the new thread as well as on the thread's lifetime.
| 线程寿命 | Development task | Solution |
|---|---|---|
| One call | Run one method within another thread and quit the thread when the method is finished. |
Qt 提供不同解决方案:
|
| One call | Operations are to be performed on all items of a container. Processing should be performed using all available cores. A common example is to produce thumbnails from a list of images. | QtConcurrent 提供 map() function for applying operations on every container element, filter() for selecting container elements, and the option of specifying a reduce function for combining the remaining elements. |
| One call | A long running operation has to be put in another thread. During the course of processing, status information should be sent to the GUI thread. | 使用 QThread , reimplement run and emit signals as needed. Connect the signals to the GUI thread's slots using queued signal/slot connections. |
| Permanent | Have an object living in another thread and let it perform different tasks upon request. This means communication to and from the worker thread is required. | Derive a class from QObject and implement the necessary slots and signals, move the object to a thread with a running event loop and communicate with the object over queued signal/slot connections. |
| Permanent | Have an object living in another thread, let the object perform repeated tasks such as polling a port and enable communication with the GUI thread. | Same as above but also use a timer in the worker thread to implement polling. However, the best solution for polling is to avoid it completely. Sometimes using QSocketNotifier is an alternative. |
QThread is a very convenient cross platform abstraction of native platform threads. Starting a thread is very simple. Let us look at a short piece of code that generates another thread which says hello in that thread and then exits.
// hellothread/hellothread.h class HelloThread : public QThread { Q_OBJECT private: void run(); };
We derive a class from QThread 并重实现 run() 方法。
// hellothread/hellothread.cpp void HelloThread::run() { qDebug() << "hello from worker thread " << thread()->currentThreadId(); }
The run method contains the code that will be run in a separate thread. In this example, a message containing the thread ID will be printed. QThread::start () will call the method in another thread.
int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); HelloThread thread; thread.start(); qDebug() << "hello from GUI thread " << app.thread()->currentThreadId(); thread.wait(); // do not exit before the thread is completed! return 0; }
To start the thread, our thread object needs to be instantiated. The start() method creates a new thread and calls the reimplemented run() method in this new thread. Right after start() is called, two program counters walk through the program code. The main function starts with only the GUI thread running and it should terminate with only the GUI thread running. Exiting the program when another thread is still busy is a programming error, and therefore, wait is called which blocks the calling thread until the run() method has completed.
This is the result of running the code:
hello from GUI thread 3079423696 hello from worker thread 3076111216
如上所述,开发者必须始终小心当从其它线程调用对象方法时。 线程亲缘关系 不改变此状况。Qt 文档编制将几种方法标记为线程安全。 postEvent() 是显著范例。可以从不同线程同时调用线程安全方法。
通常,在没有并发访问方法的情况下,在其它线程中调用对象的非线程安全方法工作数千次,在并发访问出现之前,可能导致意外行为。编写测试代码并不能完全确保线程的正确性,但仍很重要。在 Linux,Valgrind 和 Helgrind 可以帮助检测线程错误。
A mutex is an object that has lock() and unlock() methods and remembers if it is already locked. A mutex is designed to be called from multiple threads. lock() returns immediately if the mutex is not locked. The next call from another thread will find the mutex in a locked state and then lock() will block the thread until the other thread calls unlock() . This functionality can make sure that a code section will be executed by only one thread at a time.
The following line sketches how a mutex can be used to make a method thread-safe:
void Worker::work() { this->mutex.lock(); // first thread can pass, other threads will be blocked here doWork(); this->mutex.unlock(); }
What happens if one thread does not unlock a mutex? The result can be a frozen application. In the example above, an exception might be thrown and
mutex.unlock()
will never be reached. To prevent problems like this,
QMutexLocker
should be used.
void Worker::work() { QMutexLocker locker(&mutex); // Locks the mutex and unlocks when locker exits the scope doWork(); }
This looks easy, but mutexes introduce a new class of problems: deadlocks. A deadlock happens when a thread waits for a mutex to become unlocked, but the mutex remains locked because the owning thread is waiting for the first thread to unlock it. The result is a frozen application. Mutexes can be used to make a method thread safe. Most Qt methods aren't thread safe because there is always a performance penalty when using mutexes.
It isn't always possible to lock and unlock a mutex in a method. Sometimes the need to lock spans several calls. For example, modifying a container with an iterator requires a sequence of several calls which should not be interrupted by other threads. In such a scenario, locking can be achieved with a mutex that is kept outside of the object to be manipulated. With an external mutex, the duration of locking can be adjusted to the needs of the operation. One disadvantage is that external mutexes aid locking, but do not enforce it because users of the object may forget to use it.
The event loops of Qt are a very valuable tool for inter-thread communication. Every thread may have its own event loop. A safe way of calling a slot in another thread is by placing that call in another thread's event loop. This ensures that the target object finishes the method that is currently running before another method is started.
So how is it possible to put a method invocation in an event loop? Qt has two ways of doing this. One way is via queued signal-slot connections; the other way is to post an event with QCoreApplication::postEvent (). A queued signal-slot connection is a signal slot connection that is executed asynchronously. The internal implementation is based on posted events. The arguments of the signal are put into the event loop and the signal method returns immediately.
The connected slot will be executed at a time which depends on what else is in the event loop.
Communication via the event loop eliminates the deadlock problem we face when using mutexes. This is why we recommend using the event loop rather than locking an object using a mutex.
获得工作者线程结果的一种办法是等待线程终止。然而,在很多情况下,阻塞等待不可接受。阻塞等待的替代是采用发布事件、队列信号及槽异步交付结果。这会产生某些开销,因为操作结果不会出现在下一源代码行中,而是出现在定位源代码文件中某些位置的槽中。Qt 开发者习惯使用这种异步行为,因为它非常类似用于 GUI 应用程序的事件驱动编程。
This tutorial comes with examples for Qt's three basic ways of working with threads. Two more examples show how to communicate with a running thread and how a QObject can be placed in another thread, providing service to the main thread.
The following examples can all be compiled and run independently. The source can be found in the examples directory: examples/tutorials/threads/
Creating and destroying threads frequently can be expensive. To avoid the cost of thread creation, a thread pool can be used. A thread pool is a place where threads can be parked and fetched. We can write the same "hello thread" program as above using the global thread pool. We derive a class from QRunnable . The code we want to run in another thread needs to be placed in the reimplemented QRunnable::run () 方法。
// hellothreadpool/main.cpp class Work : public QRunnable { public: void run() { qDebug() << "Hello from thread " << QThread::currentThread(); } }; int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); Work work; work.setAutoDelete(false); QThreadPool *threadPool = QThreadPool::globalInstance(); threadPool->start(&work); qDebug() << "hello from GUI thread " << QThread::currentThread(); threadPool->waitForDone(); return 0; }
We instantiate Work in main(), locate the global thread pool and use the QThreadPool::start () method. Now the thread pool runs our worker in another thread. Using the thread pool has a performance advantage because threads are not destroyed after they have finished running. They are kept in a pool and wait to be used again later.
// helloconcurrent/main.cpp void hello() { qDebug() << "Hello from thread " << QThread::currentThread(); } int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QFuture<void> future = QtConcurrent::run(hello); qDebug() << "hello from GUI thread " << QThread::currentThread(); future.waitForFinished(); return 0; }
We write a global function hello() to implement the work. QtConcurrent::run () is used to run the function in another thread. The result is a QFuture . QFuture provides a method called waitForFinished() , which blocks until the calculation is completed. The real power of QtConcurrent becomes visible when data can be made available in a container. QtConcurrent provides several functions that are able to process itemized data on all available cores simultaneously. The use of QtConcurrent is very similar to applying an STL algorithm to an STL container. QtConcurrent Map is a very short and clear example about how a container of images can be scaled on all available cores. The image scaling example uses the blocking variants of the functions used. For every blocking function there is also a non-blocking, asynchronous counterpart. Getting results asynchronously is implemented with QFuture and QFutureWatcher .
We want to produce a clock application. The application has a GUI and a worker thread. The worker thread checks every 10 milliseconds what time it is. If the formatted time has changed, the result will be sent to the GUI thread where it is displayed.
Of course, this is an overly complicated way of designing a clock and, actually, a separate thread is unnecessary. We would be better off placing the timer in the main thread because the calculation made in the timer slot is very short-lived. This example is purely for instructional use and shows how to communicate from a worker thread to a GUI thread. Note that communication in this direction is easy. We only need to add a signal to QThread and make a queued signal/slot connection to the main thread. Communication from the GUI to the worker thread is shown in the next example.
int main(int argc, char *argv[]) { QApplication app(argc, argv); // build gui QWidget widget; QLabel *label = new QLabel; QHBoxLayout *layout = new QHBoxLayout(&widget); layout->addWidget(label); widget.setWindowTitle("clock"); //instantiate thread object ClockThread clockThread; QObject::connect(&clockThread, SIGNAL(sendTime(QString)), label, SLOT(setText(QString)), Qt::QueuedConnection); clockThread.start(); widget.show(); app.exec(); clockThread.quit(); clockThread.wait(); return 0; }
We've connected the
clockThread
with the label. The connection must be a queued signal-slot connection because we want to put the call in the event loop.
// clock/clockthread.h class ClockThread : public QThread { Q_OBJECT signals: void sendTime(QString time); private: void run(); QString m_lastTime; private slots: void timerHit(); };
We have derived a class from
QThread
and declared the
sendTime()
信号。
// clock/clockthread.cpp void ClockThread::run() { QTimer timer; connect(&timer, SIGNAL(timeout()), this, SLOT(timerHit()), Qt::DirectConnection); timer.setInterval(10); timer.start(); // puts one event in the threads event queue exec(); timer.stop(); } void ClockThread::timerHit() { QString newTime= QDateTime::currentDateTime().toString("ddd MMMM d yy, hh:mm:ss"); if(m_lastTime != newTime ){ m_lastTime = newTime; emit sendTime(newTime) ; } }
The trickiest part of this example is that the timer is connected to its slot via a direct connection. A default connection would produce a queued signal-slot connection because the connected objects live in different threads; remember that QThread does not live in the thread it creates.
Still it is safe to access ClockThread::timerHit() from the worker thread because ClockThread::timerHit() is private and only touches local variables and a private member that isn't touched by public methods. QDateTime::currentDateTime () isn't marked as thread-safe in Qt documentation, however we can get away with using it in this small example because we know that the QDateTime::currentDateTime () static method isn't used in any other threads.
This example shows how it is possible to have a
QObject
in a worker thread that accepts requests from the GUI thread, does polling using a timer and continuously reports results back to the GUI thread. The actual work including the polling must be implemented in a class derived from
QObject
. We have called this class
WorkerObject
in the code shown below. The thread-specific code is hidden in a class called
Thread
, derived from
QThread
.
Thread
has two additional public members. The
launchWorker()
member takes the worker object and moves it to another thread with a started event loop. The call blocks for a very short moment until the thread creation operation is completed, allowing the worker object to be used again on the next line. The
Thread
class's code is short but somewhat involved, so we only show how to use the class.
// movedobject/main.cpp int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); Thread thread; qDebug() << "main thread ID: " << app.thread()->currentThreadId(); WorkerObject *worker = new WorkerObject; thread.launchWorker(worker); QMetaObject::invokeMethod(worker, "doWork", Qt::QueuedConnection); QMetaObject::invokeMethod(worker, "startPolling", Qt::QueuedConnection, Q_ARG(int, 500)); //let application produce output for 3 seconds and quit QTimer::singleShot(3000, &app, SLOT(quit())); app.exec(); thread.stop(); thread.wait(); delete worker; return 0; }
QMetaObject::invokeMethod
() calls a slot via the event loop. The worker object's methods should not be called directly after the object has been moved to another thread. We let the worker thread do some work and polling, and use a timer to shut the application down after 3 seconds. Shutting the worker down needs some care. We call
Thread::stop()
to exit the event loop. We wait for the thread to terminate and, after this has occurred, we delete the worker.
线程是很复杂的主题。Qt 提供更多线程类,相比在此教程中呈现的。以下材料可帮您更深入研究该主题: