线程基础
线程与并行处理任务息息相关,就像进程一样。那么,线程与进程有什么区别呢?当你在电子表格上进行数据结算的时候,在相同的桌面上可能有一个播放器正在播放你最喜欢的歌曲。这是一个两个进程并行工作的例子:一个进程运行电子表格程序;另一个进程运行一个媒体播放器。这种情况最适合用多任务这个词来描述。进一步观察媒体播放器,你会发现在这个进程内,又存在并行的工作。当媒体播放器向音频驱动发送音乐数据的时候,用户界面上与之相关的信息不断地进行更新。这就是单个进程内的并行线程。
那么,线程的并行性是如何实现的呢?在单核CPU计算机上,并行工作类似在电影院中不停移动图像产生的一种假象。对于进程而言,在很短的时间内中断占有处理器的进程就形成了这种假象。然而,处理器迁移到下一个进程。为了在不同进程之间进行切换,当前程序计算器被保存,下一个程序计算器被加载进来。这还不够,相关寄存器以及一些体系结构和操作系统特定的数据也要进行保存和重新加载。
就像一个CPU可以支撑两个或多个进程一样,同样也可以让CPU在单个进程内运行不同的代码片段。当一个进程启动时,它问题执行一个代码片断从而该进程就被认为是拥有了一个线程。但是,该程序可以会决定启动第二个线程。这样,在一个进程内部,两个不同的代码序列就需要被同步处理。通过不停地保存当前线程的程序计数器和相关寄存器,同时加载下一个线程的程序计数器和相关寄存器,就可以在单核CPU上实现并行。在不同活跃线程之间的切换不需要这些线程之间的任何协作。当切换到下一个线程时,当前线程可能处于任一种状态。
当前CPU设计的趋势是拥有多个核。一个典型的单线程应用程序只能利用一个核。但是,一个多线程程序可被分配给多个核,便得程序以一种完全并行的方式运行。这样,将一个任务分配给多个线程使得程序在多核CPU计算机上的运行速度比传统的单核CPU计算机上的运行速度快很多。
如上所述,每个程序启动后就会拥有一个线程。该线程称为”主线程”(在Qt应用程序中也叫”GUI线程”)。Qt GUI必须运行在此线程上。所有的图形元件和几个相关的类,如QPixmap,不能工作于非主线程中。非主线程通常称为”工作者线程”,因为它主要处理从主线程中卸下的一些工作。
每个线程都有自己的栈,这意味着每个线程都拥有自己的调用历史和本地变量。不同于进程,同一进程下的线程之间共享相同的地址空间。下图显示了内存中的线程块图。非活跃线程的程序计数器和相关寄存器通常保存在内核空间中。对每个线程来说,存在一个共享的代码片段和一个单独的栈。
如果两个线程拥有一个指向相同对象的指针,那么两个线程可以同时去访问该对象,这可以破坏该对象的完整性。很容易想象的情形是一个对象的两个方法同时执行可能会出错。
有时,从不同线程中访问一个对象是不可避免的。例如,当位于不同线程中的许多对象之间需要进行通信时。由于线程之间使用相同的地址空间,线程之间进行数据交换要比进程之间进行数据交换快得多。数据不需要序列化然后拷贝。线程之间传递指针是允许的,但是必须严格协调哪些线程使用哪些指针。禁止在同一对象上执行同步操作。有一些方法可以实现这种要求,下面描述其中的一些方法。
那么,怎样做才安全呢?在一个线程中创建的所有对象在线程内部使用是安全的,前提条件是其他线程没有引用该线程中创建的一些对象且这些对象与其他的线程之间没有隐性耦合关系。当数据作为静态成员变量,单例或全局数据方式共享时,这种隐性耦合是可能发生的。
基本上,对线程来讲,有两种使用情形:
· 利用多核处理器使处理速度更快。
· 将一些处理时间较长或阻塞的任务移交给其他的线程,从而保证GUI线程或其他对时间敏感的线程保持良好的反应速度。
何时不应使用线程
开发者在使用线程时必须特意小心。启动其他线程很容易,但很难保证所有共享的数据仍然是一致的。这些问题通常很难找到,因为它们可以在某个时候仅显示一次或仅在某种硬件配置下出现。在创建线程解决某些问题之前,如下的一些方法也应该考虑一下。
说明 |
|
在一个耗时的计算中不停地调用QEventLoop::processEvents()能以免GUI被阻塞。但是,这种解决方式并不能用于更大范围的计算操作中,因为会导致调用 processEvents()太频繁或不够,取决于硬件。. |
|
有时,在后台进程中使用一个计时器来调度在将来某个时间点运行一段程序非常方便。超时时间为0的计时器将在事件处理完后立即触发。 |
|
当在一个低速的网络连接上进行阻塞读的时候,可以不使用多线程。只要对一块网络数据的计算可以很快地执行,那么,这种交互式的设计比线程中的同步等待要好些。交互式设计比多线程要不容易出错且更有效。在许多情况下,也有一些性能上的提升。 |
一般来讲,建议只使用安全的且已被验证过的路径,避免引入线程概念。 QtConcurrent提供了一种简易的接口,来将工作分配到所有的处理器的核上。线程相关代码已经完全隐藏在QtConcurrent 框架中,因此,开发者不需要关注这些细节。但是, QtConcurrent 不能用于那么需要与运行中的线程进行通信的情形,且它也不能用于处理阻塞操作。
有时,我们不仅仅只是在另一个线程中运行一个方法。可能需要位于其他线程中的某个对象为GUI线程提供服务。也许,你想其他的线程一直保持活跃状态去不停地轮询硬件端口并在一个需要关注的事件发生时发送一个信号给GUI线程。Qt提供了不同的解决方案来开发多线程应用程序。正确的解决方案取决于新线程的目的以及它的生命周期。
线程的生命周期 |
开发任务 |
解决方案 |
在其他的线程中运行一个方法,当方法运行结束后退出线程。 |
Qt 提供了不同的解决方案: · 1.编写一个函数,然后利用 QtConcurrent::run()运行它。 · 2.从QRunnable 派生一个类,并利用全局线程池QThreadPool::globalInstance()->start()来运行它。 · 3. 从QThread派生一个类, 重载QThread::run() 方法并使用QThread::start()来运行它。 |
|
单次调用 |
在容器中的所有项执行相同的一些操作。执行过程中使用所有可用的核。一个通用的例子就是从一个图像列表中产生缩略图。 |
QtConcurrent 提供了 map()函数来将这些操作应用于于容器中的每个项中,filter() 用于选择容器元素,以及指定一个删减函数的选项来与容器中剩下的元素进行合并。 |
单次调用 |
一个耗时的操作必须放到另一个线程中运行。在这期间,状态信息必须发送到GUI线程中。 |
使用 QThread,,重载run方法并根据情况发送信号。.使用queued信号/槽连接来连接信号与GUI线程的槽。 |
常驻 |
有一对象位于另一个线程中,将让其根据不同的请求执行不同的操作。这意味与工作者线程之间的通信是必须的。 |
从QObject 派生一个类并实现必要的槽和信号,将对象移到一个具有事件循环的线程中,并通过queued信号/槽连接与对象进行通信。 |
常驻 |
对象位于另一个线程中,对象不断执行重复的任务如轮询某个端口,并与GUI线程进行通信。 |
与上述类似,但同时在工作者线程中使用一个计时器来实现轮询。但是,最好的解决方案是完全避免轮询。有时,使用 QSocketNotifier 是一种不错的选择。 |
Qt 线程基础
QThread 是对本地平台线程的一个非常好的跨平台抽象。启动一个线程非常简单。让我们看一段代码,它产生另一个线程,该线程打印hello,然后退出。
- // hellothread/hellothread.h
- class HelloThread : public QThread
- {
- Q_OBJECT
- private:
- void run();
- };
- // hellothread/hellothread.cpp
- void HelloThread::run()
- {
- qDebug() << "hello from worker thread " << thread()->currentThreadId();
- }
run方法中包含的代码会运行于一个单独的线程。在本例中,一条包含线程ID的信号将会被输出来。QThread::start() 会在另一个线程中调用该方法。
- 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!