(转自:http://my.oschina.net/laopiao/blog/88158)算法
何谓线程?编程
线程与并行处理任务息息相关,就像进程同样。那么,线程与进程有什么区别呢?当你在电子表格上进行数据结算的时候,在相同的桌面上可能有一个播放器正在播放你最喜欢的歌曲。这是一个两个进程并行工做的例子:一个进程运行电子表格程序;另外一个进程运行一个媒体播放器。这种状况最适合用多任务这个词来描述。进一步观察媒体播放器,你会发如今这个进程内,又存在并行的工做。当媒体播放器向音频驱动发送音乐数据的时候,用户界面上与之相关的信息不断地进行更新。这就是单个进程内的并行线程。安全
那么,线程的并行性是如何实现的呢?在单核CPU计算机上,并行工做相似在电影院中不停移动图像产生的一种假象。对于进程而言,在很短的时间内中断占有处理器的进程就造成了这种假象。然而,处理器迁移到下一个进程。为了在不一样进程之间进行切换,当前程序计算器被保存,下一个程序计算器被加载进来。这还不够,相关寄存器以及一些体系结构和操做系统特定的数据也要进行保存和从新加载。网络
就像一个CPU能够支撑两个或多个进程同样,一样也可让CPU在单个进程内运行不一样的代码片断。当一个进程启动时,它问题执行一个代码片段从而该进程就被认为是拥有了一个线程。可是,该程序能够会决定启动第二个线程。这样,在一个进程内部,两个不一样的代码序列就须要被同步处理。经过不停地保存当前线程的程序计数器和相关寄存器,同时加载下一个线程的程序计数器和相关寄存器,就能够在单核CPU上实现并行。在不一样活跃线程之间的切换不须要这些线程之间的任何协做。当切换到下一个线程时,当前线程可能处于任一种状态。多线程
当前CPU设计的趋势是拥有多个核。一个典型的单线程应用程序只能利用一个核。可是,一个多线程程序可被分配给多个核,便得程序以一种彻底并行的方式运行。这样,将一个任务分配给多个线程使得程序在多核CPU计算机上的运行速度比传统的单核CPU计算机上的运行速度快不少。并发
GUI 线程和工做者线程app
如上所述,每一个程序启动后就会拥有一个线程。该线程称为”主线程”(在Qt应用程序中也叫”GUI线程”)。Qt GUI必须运行在此线程上。全部的图形元件和几个相关的类,如QPixmap,不能工做于非主线程中。非主线程一般称为”工做者线程”,由于它主要处理从主线程中卸下的一些工做。框架
数据的同步访问异步
每一个线程都有本身的栈,这意味着每一个线程都拥有本身的调用历史和本地变量。不一样于进程,同一进程下的线程之间共享相同的地址空间。下图显示了内存中的线程块图。非活跃线程的程序计数器和相关寄存器一般保存在内核空间中。对每一个线程来讲,存在一个共享的代码片断和一个单独的栈。函数
若是两个线程拥有一个指向相同对象的指针,那么两个线程能够同时去访问该对象,这能够破坏该对象的完整性。很容易想象的情形是一个对象的两个方法同时执行可能会出错。
有时,从不一样线程中访问一个对象是不可避免的。例如,当位于不一样线程中的许多对象之间须要进行通讯时。因为线程之间使用相同的地址空间,线程之间进行数据交换要比进程之间进行数据交换快得多。数据不须要序列化而后拷贝。线程之间传递指针是容许的,可是必须严格协调哪些线程使用哪些指针。禁止在同一对象上执行同步操做。有一些方法能够实现这种要求,下面描述其中的一些方法。
那么,怎样作才安全呢?在一个线程中建立的全部对象在线程内部使用是安全的,前提条件是其余线程没有引用该线程中建立的一些对象且这些对象与其余的线程之间没有隐性耦合关系。当数据做为静态成员变量,单例或全局数据方式共享时,这种隐性耦合是可能发生的。
使用线程
基本上,对线程来说,有两种使用情形:
· 利用多核处理器使处理速度更快。
· 将一些处理时间较长或阻塞的任务移交给其余的线程,从而保证GUI线程或其余对时间敏感的线程保持良好的反应速度。
什么时候不该使用线程
开发者在使用线程时必须特地当心。启动其余线程很容易,但很难保证全部共享的数据仍然是一致的。这些问题一般很难找到,由于它们能够在某个时候仅显示一次或仅在某种硬件配置下出现。在建立线程解决某些问题以前,以下的一些方法也应该考虑一下。
非线程方式 |
说明 |
QEventLoop::processEvents() |
在一个耗时的计算中不停地调用QEventLoop::processEvents()能以避免GUI被阻塞。可是,这种解决方式并不能用于更大范围的计算操做中,由于会致使调用 processEvents()太频繁或不够,取决于硬件。. |
QTimer |
有时,在后台进程中使用一个计时器来调度在未来某个时间点运行一段程序很是方便。超时时间为0的计时器将在事件处理完后当即触发。 |
QSocketNotifierQNetworkAccessManagerQIODevice::readyRead() |
当在一个低速的网络链接上进行阻塞读的时候,能够不使用多线程。只要对一块网络数据的计算能够很快地执行,那么,这种交互式的设计比线程中的同步等待要好些。交互式设计比多线程要不容易出错且更有效。在许多状况下,也有一些性能上的提高。 |
通常来说,建议只使用安全的且已被验证过的路径,避免引入线程概念。 QtConcurrent提供了一种简易的接口,来将工做分配到全部的处理器的核上。线程相关代码已经彻底隐藏在QtConcurrent 框架中,所以,开发者不须要关注这些细节。可是, QtConcurrent 不能用于那么须要与运行中的线程进行通讯的情形,且它也不能用于处理阻塞操做。
该使用哪一种 Qt 线程技术?
有时,咱们不只仅只是在另外一个线程中运行一个方法。可能须要位于其余线程中的某个对象为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,而后退出。
咱们从QThread 中派生一个类并重载run()方法。
run方法中包含的代码会运行于一个单独的线程。在本例中,一条包含线程ID的信号将会被输出来。QThread::start() 会在另外一个线程中调用该方法。
为了启动该线程,咱们的线程对象必须被初始化。start() 方法建立了一个新的线程并在新线程中调用重载的run() 方法。 在 start() 被调用后,有两个程序计数器走过程序代码。主函数启动,且仅有一个GUI线程运行,它中止时也只有一个GUI线程运行。当另外一个线程仍然忙碌时退出程序是一种编程错误,所以, wait方法被调用用来阻塞调用的线程直到run()方法执行完毕。
下面是运行代码的结果:
hello from GUI thread 3079423696
hello from worker thread 3076111216
QObject 和线程
一个 QObject 一般被认为有线程亲和力 或换句话说, 它位于某个线程中。这意味着,在建立的时候, QObject保存了一个指向当前线程的指针。当一个事件利用 postEvent()发出时,该信息就变得有关了。该事件将会被放于对应线程的事件循环中。若是QObject位于的线程没有事件循环,那么事件就不会被传递。
为了启动一个事件循环,exec() 必须在 run()里面调用. 线程亲和力可以使用moveToThread()来改变。如上所述,开发者从其余线程中调用对象的方法时必须很是当心。线程亲和力并无改变这种情况。Qt文档标记了几个方法是线程安全的。 postEvent() 是一个很明显的例子。一个线程安全的方法能够在不一样的线程中同时被调用。
在没有并行访问方法的状况下,在其余线程中调用对象的非线程安全的方法时可能运行了几千次后才会出现一个并发访问,形成不可预料的行为。编写测试代码并不能彻底的保证线程的正确性,但仍然很重要。在Linux中,Valgrind和Helgrind能够侦测线程错误。
QThread 细节很是有意思:
· QThread 并不位于新线程 run()执行的位置中。它位于旧线程中。
· 大部分QThread 的方法是线程的控制接口中,并在旧线程中调用。不要使用moveToThread()将这些接口移到新建立的线程中,例如,调用moveToThread(this) 被认为是一种坏的实践。
· exec()和静态方法usleep(), msleep(), sleep()应在新建立的线程中调用。
其余的一些定义在 QThread 子类中的成员能够在新旧线程中访问。开发者负责协调这些访问。 一种典型的策略是在调用 start() 前设置这些成员。一旦工做者线程运行起来,主线程不该当再修改这些成员。当工做者线程中止后,主线程又能够访问些额外的成员。这是一种在线程启动前和中止后传递参数的方便的策略。
一个 QObject's 父类必须位于相同的线程中。对于run()方法中建立的对象,在这有一个很是惊人的结果。
使用一个互斥量 来保护数据的完整性
一个互斥量是一中且具备lock() 和 unlock() 方法的对象,并记住它是否被锁住。互斥量可在多个线程中访问。若是互斥量没有被锁定, lock() 会当即返回。下一个从其余线程的调用会发现互斥量已经处于锁定状态,而后,lock() 会阻塞线程直到其余线程调用 unlock()。该功能可保证一个代码段在同一时间仅能被一个线程执行。
下面代码显示了怎样使用一个互斥量来确保一个方法是线程安全的。
若是一个线程不能解锁一个互斥量会发生什么状况呢?结果是应用程序会僵死。在上面的例子中,能够会抛出异常且永远不会到达mutex.unlock() 。为了防止这种状况,应该使用 QMutexLocker 。
这看上去很简单,但互斥会引入新的问题:死锁。当一个线程等待一个互斥量变为解锁,可是该互斥量仍然处于锁定状态,由于占有该互斥量的线程在等待第一个线程解锁该互斥量。结果是一个僵死的应用程序。互斥量用于保证一个方法是线程安全的。大部分Qt方法不是线程安全的,由于当使用互斥量时老是有些性能损失。
在一个方法中并不老是可以加锁和解锁一个互斥量。有时,锁定的范围跨越了数个调用。例如,利用迭代器修改一个容器时须要几个调用组成的序列,这个序列不能被其余线程中断。在这种状况下,利用外部锁就能够保证这个调用序列是被锁定的。利用一个外部锁,锁定的时间能够根据操做的须要进行调整。很差之处是外部锁帮助锁定,但不能强制执行它,由于对象的使用者可能忘记使用它。
使用事件循环来防止数据崩溃
Qt的事件循环对线程间通讯是一个很是有价值的工具。每一个线程能够拥有本身的事件循环。调用另外一个线程中的槽的安全方法就是将此调用放在该线程的事件循环中。这确保了目标对象在启动另外一方法前完成了当前正在执行的方法。那么,怎样将一个方法调用放到一个事件循环中呢?Qt有两种方式。一种方式是经过queued信号-槽链接;另外一种方式就是利用QCoreApplication::postEvent()发送一个事件。一个queued 信号-槽链接是一种异步执行的信号槽链接。内部实现是基于发送的事件。信号的参数放置到事件循环中,信号方法会当即返回。
链接的槽执行的时间取决于事件循环中的基于事件。经过事件循环通讯消除了使用互斥量面临的死锁问题。这就是为何咱们建议使用事件循环而不是使用互斥量锁定一个对象。
处理异步执行
一种得到工做者线程结果的方式是等待该线程中止。然而,在许多状况下,阻塞的等待是不可接受的。另外一种方式是经过发送的事件或queued信号和槽来得到异步结果。这产生了一些开销,由于一个操做的结果并非出如今下一个代码行,而是在一个位于其余地方的槽中。Qt开发者习惯了这种异步行为,由于它与GUI应用程序中事件驱动的方式很是相似。
例子
该手册提供了一些例子,演示了在Qt中使用线程的三种基本方法。另外两个例子演示了怎样与一个运行中的线程进行通讯以及一个 QObject 可被置于另外一个线程中,为主线程提供服务。
· 使用 QThread 使用如上所示。
· 使用全局的QThreadPool
· 使用 QtConcurrent
· 与GUI线程进行通讯
· 在另外一个线程的常驻对象为主线程提供服务
以下 的例子能够单独地进行编译和运行。源码可在源码目录中找到:examples/tutorials/threads/
例 1: 使用Thread Pool
不停地建立和销毁线程很是耗时,可使用一个线程池。线程池能够存取线程和获取线程。咱们可使用全局线程池写一个与上面相同的"hello thread" 程序 。咱们从QRunnable派生出一个类。在另外一个线程中运行的代码必须放在重载的QRunnable::run()方法中。
在main()中, 咱们实例化了Work, 定位于全局的线程池,使用QThreadPool::start()方法。如今,线程池在另外一个线程中运行咱们的工做。 使用线程池有一些性能上的优点,由于线程在它们结束后没有被销毁,它们被保留在线程池中,等待以后再次被使用。
例 2: 使用 QtConcurrent
咱们写一个全局的函数hello()来实现工做者代码。QtConcurrent::run()用于在另外一个线程中运行该函数。该结果是QFuture。 QFuture 提供了一个方法叫waitForFinished(), 它阻塞主线程直到计算完成。当所需的数据位于容器中时,QtConcurrent才显示它真正的威力。 QtConcurrent 提供了一些函数能并行地处理这些已经成为容器里元素的一些数据。使用QtConcurrent很是相似于应用一个STL算法到某个STL容器类。QtConcurrent Map是一个很是简短且清晰的例子,它演示了容器中的图片怎么被扩散到全部核中去处理。对于每一个阻塞函数,都同时存在一个非阻塞, 异步型函数。异步地获取结果是经过QFuture 和QFutureWatcher来实现的。
例 3: Clock
咱们想建立一个时钟应用程序。该应用程序有一个GUI和一个工做者线程。工做者线程每10毫秒检查一下当前的时间。若是格式化的时间发生了变化,该结果会发送给显示时间的GUI线程当中。
固然, 这是一种过分复杂的方式来设计一个时钟,事实上,一个独立的线程不必。使用计时器会更好。本例子纯粹是用于教学目的的,演示了从工做者线程向GUI线程进行通讯。 注意,这种通讯方式很是容易,咱们仅须要添加一个信号给QThread, 而后构建一个queued 信号/槽链接到主线程中。从GUI到 工做者线程的方式在下一个例子中演示。
咱们已经将 clockThread 与标签链接起来。链接必须是一个queued 信号-槽链接,由于咱们想将调用放到事件循环当中。
咱们从 QThread 派生出一个类,并声明sendTime()信号。
该例子中最值得关注的部分是计时器经过一个直接链接与它的槽相连。默认的链接会产生一个queued 信号-槽链接,由于被链接的对象位于不一样的线程。记住,QThread并不位于它建立的线程中。可是,从工做者线程中访问ClockThread::timerHit() 仍然是安全的,由于ClockThread::timerHit()是私有的,且只处理私有变量。QDateTime::currentDateTime() 在Qt文档中并未标记为线程安全的,可是在此例子中,咱们能够放心使用,由于咱们知道访方法没有会其余的线程中使用。
例 4: A 常驻线程
该例子演示了位于工做者线程中的一个QObject接受来自GUI线程的请求,利用一个计时器进行轮询,并不时地将结果返回给GUI线程。实现的工做包括轮询必须实如今一个从QObject派生出的类中。在以下代码中,咱们已称该类为 WorkerObject。 线程相关的代码已经隐藏在称为Thread类中,派生自QThread. Thread有两个额外的公共成员。launchWorker() 获取工做者对象并将其移到另外一个开启了事件循环的线程中。 该调用阻塞一小会,直到建立操做完成,使得工做者对象能够在下一行被再次使用。Thread 类的代码短但有点复杂,所以咱们只显示怎样使用该类。
QMetaObject::invokeMethod()经过事件循环调用槽。worker对象的方法不该该在对象被移动到另外一个线程中直接调用。咱们让工做者线程执行一个工做和轮询,并使用一个计时器在3秒后关闭该应用程序。关闭worker须要小心。咱们调用 Thread::stop() 退出事件循环。咱们等待线程中止,当线程中止后,咱们删除worker。