Qt知识回顾(十六)——进程和线程

进程

  设计应用程序时,有时不希望将一个不太相关的功能集成到程序中,或者是因为该功能与当前设计的应用程序联系不大,或者是因

运行一个进程

  Qt的QProcess类用来启动一个外部程序并与其进行通信。要启动一个进程,可以使用start()函数,然后将程序名称和运行这个程序所要使用的命令行参数作为该函数的参数。执行完start()函数后,QProcess进入Starting状态,当程序已经运行后,QProcess就会进入Running状态并发射started()信号,当进程退出后,QProcess重新进入NotRunning状态(初始状态)并发射finished()信号。发射的finished()信号提供了进程的退出代码和退出状态,也可以调用exitCode()来获取上一个结束的进程的退出代码。使用exitStatus()来获取它的退出状态。任何时间发生了错误,QProcess都会发射error()信号,也可以调用error()来查看错误的类型和上次发生的错误。使用state()可以查看当前进程的状态。
  QProcess允许将一个进程视为一个顺序I/O设备。可以像使用QTcpSocket访问一个网络连接一样来读/写一个进程。可以调用write()向进程的标准输入进行写入,调用read()、readLine()和getChar()等从标准输出进行读取。因为QProcess继承自QIODevice,它也可以作为QXmlReader的数据源,或者为QNetworkAccessManager产生用于上传的数据。

进程间通信

  Qt提供了多种方法在Qt应用程序中是西安进程间通信IPC,简单介绍如下:
  1)TCP/IP
  跨平台的Qt Network模块提供了众多的类来实现网络编程。它提供了高层的类(比如QNetworkAccessManager等)来使用特定的应用程序级协议,也提供了较低的类(例如,QTcpSocket、QTcpServer和QSslSocket)来实现相关协议。
  2)共享内存
  QSharedMemory是跨平台的共享内存类,提供了访问操作系统共享内存的实现。它允许多个线程和进程安全地访问共享内存段。此外,QSystemSemaphore可用于控制系统的共享资源的访问以及进程间通信。
  3)D-Bus
  Qt D-Bus模块是一个Unix库,可以使用D-Bus协议来实现进程间通信。它将Qt的信号和槽机制扩展了IPC层面,允许从一个进程发射的信号关联到另一个进程的槽上,可以在帮助中查看D-Bus关键字。
  4)QProcess
  5)会话管理
  在Linux/X11平台上,Qt提供了对会话管理的支持,会话允许事件传播到进程。例如,当关机时通知进程或程序,从而可以执行一些相关的操作。具体内容可以参考Session Management关键字。
  Qt中还有一个QLocalSocket类提供了一个本地套接字,在Windows上,它是一个有名管道;在Unix上,它是一个本地域套接字。与其对应的是QLocalServer类,它提供了一个基于本地套接字的服务器。

线程

  Qt提供了对线程的支持,这包括一组与平台无关的线程类、一个线程安全的发送事件的方式以及跨线程的信号-槽的关联。这些使得可以很轻松地开发可以指的多线程Qt应用程序,可以充分利用多处理器的机器。多线程编程也可以有效解决在不冻结一个应用程序用户界面的情况下执行一个耗时操作的问题。可通过Thread Support in Qt关键字查看。

使用QThread启动线程

  Qt中的QThread类提供了与平台无关的线程。一个QThread代表了一个在应用程序中可以独立控制的线程,它与进程中的其他线程分享数据,但是是独立执行的。相对于一般的程序都是从main()函数开始执行,QThread从run()函数开始执行。默认的,run()通过调用exec来开始事件循环,并在线程内运行一个Qt事件循环。要创建一个线程,需要子类化QThread,并且重新实现run()函数。当从run()函数返回后,线程便执行结束,就像应用程序离开main()函数一样。QThread会在开始、结束和终止时发射started()、finshed()和terminated()等信号。也可以使用isFinished()和isRunning()来查询线程的状态。可以使用wait()来阻塞,直到线程结束执行。每个线程都会操作系统获得自己的堆栈,操作系统会决定堆栈的默认大小,也可以使用setStackSize()来设置一个自定义的堆栈大小。
  每一个线程可以有自己的事件循环,可以通过调用exec()函数来启动事件循环,可以通过调用exit()或者quit()来停止事件循环。线程中拥有一个事件循环,使它能够关联其他线程中的信号到本线程的槽上,这使用了队列关联机制,就是在使用connect()函数进行信号和槽的关联时,将Qt::ConnectionType类型的参数指定为Qt::QueuedConnection。拥有事件循环还可以使该线程能够使用需要事件循环的类,比如QTimer和QTcpSockedt类等。注意,在线程中是无法使用任何界面部件类的。
  在极端情况下,可能想要强制终止一个正在执行的线程,这时可以使用termainate()函数。但是,线程是否会被立即终止,依赖于操作系统的调度策略。可以在调用完terminate()后调用QThread::wait()来同步终止。使用terminate()函数时,线程可能在任何时刻被终止而无法斤西瓜一些清理工作。因此,该函数时很危险的,一般不建议使用,只有在绝对必要的时候使用。
  静态函数currentThreadId()和currentThread()可以返回当前执行的线程的标识符,前者返回一个该线程的系统特定的ID;后者返回一个QThread指针。QThread也提供了多个平台无关的睡眠函数,其中,sleep()精度为妙,msleep()精度为毫秒,usleep()精度为微妙。

同步线程

  Qt中的QMutex、QReadWriteLock、QSemaphore和QWaitCondition类提供了同步线程的方法。虽然使用线程的思想是多个线程可以尽可能地并发执行,但是总有一些时刻,一些线程必须停止来等待其他线程。例如,如果两个线程尝试同时访问相同的全局变量,则结果通常是不确定的。
  QMutex提供了一个互斥锁,在任何时间至多有一个线程可以获得mutex。如果一个线程尝试获得mutex,而此时mutex已经被锁住了,则这个线程将会睡眠,直到现在获得mutex的线程对mutex进行解锁位置。互斥锁经常用于对共享数据的访问进行保护。
  QReadWriteLock即读写锁,与QMetex很相似,只不过他将对共享数据的访问区分为“读”访问和“写”访问,允许多个线程同时对数据进行“读”访问。在可能的情况下使用QReadWriteLock代替QMutex,可以提高多线程程度的并发度。
  QSemaphore即信号量,是QMutex的一般化,它用来保护一定数量的相同资源,而互斥锁mutex只能保护一个资源。
  QWaitCondition类即条件变量,允许一个线程在一些条件满足时唤醒其他的线程。一个或者多个线程可以被阻塞来等待一个QWaitCondition,从而设置一个用于wakeOne()可以唤醒一个随机选取的等待的线程,而使用wakeAll()可以唤醒所有正在等待的线程。这个类的使用可以参考帮助文档,也可以参考Qt提供的 Wait Conditions Example实例程序。

可重入与线程安全

  在Qt文档中,术语“可重入”和“线程安全”用来标记类的函数,从而表明怎样在多线程应用程序中使用它们:
  >一个线程安全地函数可以同时被多个线程调用,即使是它们使用了共享数据。因为该共享数据的所有实例都被序列化了。
  >一个可重入的函数也可以同时被多个线程调用,但是只能是在每个调用使用自己的数据的情况下。
  因此一个线程安全的函数总是可重入的,但是一个可重入的函数不总是线程安全的。推而广之,如果每个线程使用一个类的不同的实例,则该类的成员函数可以被多个线程安全地调用,那么这个类被称为可重入地;如果即使所有的线程使用一个类的相同的实例,该类的成员函数也可以被多个线程安全地调用,那么这个类被称为线程安全的。注意:只有在文档中标记为线程安全的Qt类,才可以用于多线程。所以,如果一个函数没有被标记为线程安全的或者可重入的,则它不应该被多个线程使用;如果一个类没有标记为线程安全的或者可重入的,则该类的一个特定的实例不应该被多个线程访问。

1.可重入
  C++类一般是可重入的,因为它们只访问自己的数据成员。任何线程都可以调用可重入类实例的成员函数,只要没有其他线程在同一事件调用该类的相同实例的成员函数即可。例如,下面的Counter类是可重入的:

class Counter
{
  public:
    Counter(){n = 0;}
    void increment() {++n;}
    void decrement() {--n;}
    int value() const { return n;}
  private:
    int n;
};

  这个类并不是线程安全的,因为如果多个线程尝试修改数据成员n,结果便是不可预测的。这是因为++和–操作并不总是原子的。事实上,他们会被分为3个机器指令:第一条指令向寄存器中加载变量的值;第二条指令递增或者递减寄存器的值;第三条指令将寄存器的值存储到内存中。如果线程A和线程B同时加载了变量的旧值,然后递增它们的寄存器并存储回去,则它们相互覆盖,结果变量只递增了一次。

2.线程安全
  对于前面的线程A和线程B的情况,很明显,访问应该按顺序进行:在线程B执行相同的操作前,线程A必须执行完3条机器指令而不能被中断,反之亦然。一个简单的方法来使类成为线程安全的,就是使用QMutex来保护数据成员的所有访问:

class Counter
{
  public:
    Counter(){n = 0;}
    void increment() {QMutexLocker locker(&mutex); ++n;}
    void decrement() {QMutexLocker locker(&mutex);--n;}
    int value() const {QMutexLocker locker(&mutex);return n;}
  private:
    mutable QMutex mutex;
    int n;
};

  这里的QMutexLocket类在其构造函数中自动锁住mutex,然后在析构函数进行调用时对其进行解锁,在函数结束后会调用析构函数。锁住mutex确保了不同线程的访问可以序列化进行。数据成员mutex使用了mutable限定符,是因为需要在value()函数中对mutex进行加锁和解锁,而它是一个const函数。

线程和QObjects

  QThread继承自QObject,他发射信号来告知线程的开始和结束执行等状态,它也提供了一些槽。QObject可以在多线程中使用,发射信号来调用其他线程中的槽,而且向其他线程中的对象发射事件。而这些实现的基础是每一个线程都允许有自己的事件循环。

1.QObject的可重入性
  QObject是可重入的。它的大多数非GUI子类,例如,QTimer、QTcpSocket、QUdpSocket和QProcess,也都是可重入的,可以在多个线程中同时使用这些类。注意,这些类是被设计为在单一的线程中进行创建和使用的,在一个线程中创建一个对象,然后再另外一个线程中调用这个对象的一个函数是无法保证一定可以工作的。需要注意3个约束条件:
  >QObject的子对象必须在创建它的父对象的线程中创建。这意味着,永远不要将QThread对象(this)作为在该线程中创建的对象的父对象。
  >事件驱动对象只能在单一的线程中使用。具体来说,这条规则应用在定时器机制和网络模块中。例如,不可以在对象所在的线程QObject::thread()以外的其他线程中启动一个定时器或者连接一个套接字。
  >必须确保删除QThread对象以前删除在该线程中创建的所有对象,可以通过在run()函数中的栈上创建对象来保证这一点。
  虽然QObject是可重入的,但是对于GUI类,尤其是QWidget及其所有子类,是不可重入的,它们只能在主线程中使用。QCoreApplication::exec()也必须在主线程中调用。在实际应用中,无法在主线程以外的线程中使用GUI类的问题,可以简单的通过这样的方式解决:将一些非常耗时的操作放在一个单独的工作线程中来进行,等该工作线程完成后将结果返回给主线程,最后由主线程将结果显示到屏幕上。

2.每个线程的事件循环
  每一个线程都可以由它自己的事件循环。初始化线程使用QCoreApplication::exec()来开启它的事件循环;其他的线程可以使用QThead::exec()来开启一个事件循环,如下图所示。与QCoreApplication相似,QThread提供了一个exit()函数和一个quit()槽。在一个线程中使用事件循环,使得该线程可以使用那些需要事件循环的非GUI类。也使得该线程可以关联任意一个线程的信号到一个指定线程的槽。
在这里插入图片描述
在这里插入图片描述
  如果在一个线程中创建了一个QObject对象,那么这个QObject对象被称为居住在该线程。发射这个对象的事件由该线程的事件循环进行分派。可以使用QObject::thread()获得该对象所在的线程。可以使用QObject::movetoThread()来改变对象及其孩子所在的线程。
  在其他线程中调用delete来删除该QObject对象是不安全的,除非可以确保该对象当前没有在处理事件。可以使用QObject::deleteLater()来代替,这样会发送一个DeferredDelete事件,最终该对象所在线程的事件循环将会获取该事件。默认的,拥有该QObject对象的线程就是创建该QObject对象的线程,只要没有调用过QObject::moveToThread()函数。
  如果没有运行事件循环,则事件将不会传送到对象。例如,如果在一个线程中创建了一个QTimer对象,但是从来没有调用exec()函数,那么QTimer将永远不会发射它的timeout()信号,调用deleteLater()也不会工作。
  可以手动使用线程安全函数QCoreApplication::postEvent()在任何时间向任何线程的任何对象发射时间。该事件将会被创建该对象的线程的事件循环进行分派。
  所有的线程都支持事件过滤器,但是被监视的对象必须与监控对象在同一个线程中。相似的,QCoreApplication::sendEvent()与(postEvent()不同)只能向调用该函数的线程中的对象分派事件。

3.从其他线程中访问QObject子类
  QObject和它所有的子类都不是线程安全的,这包括了整个事件传递系统。需要时刻记得,正在从其他线程中访问一个对象时,事件循环可能正在向这个对象传递事件。
  如果调用一个没有在当前线程中的QObject子类的函数,而这个对象有可能会获取事件,那么就必须使用mutex来保护对这个QObject子类的内部数据的所有访问,否则,可能出现崩溃或者其他意外行为。
  与其他对象相似,QThread对象居住于创建该对象的线程。在QThread子类中提供槽一般是不安全的,除非使用mutex来保护成员变量。但是,可以安全地在QThread::run()函数中发射信号,因为信号发射是线程安全的。

4.跨线程的信号和槽
  Qt支持几种信号和槽关联类型:
  >Auto Connection(默认)。如果信号发射和信号接收的对象在同一个线程,那么执行方式与Direct Connection相同,否则,执行方式与Queued Connection相同。
  >Direct Connection。信号发射后,槽立即被调用。槽在信号发送者的线程中执行,而接收者并非必须在同一个线程。
  >Queued Connection。控制权返回到接收者线程的事件循环后才调用槽。槽在接收者的线程中被执行。
  >Blocking Queued Connection。槽的调用与Queued Connection相同,不同的是当前线程会阻塞直到槽返回。注意:使用这种方式关联在相同线程中的对象时,会引起死锁。
  Unique Connection。执行方式与Auto Connection相同。只不过关联必须是唯一的。例如,如果在相同的两个对象之间,相同的信号已经关联到了相同的槽上,那么这个关联既不是唯一的,这时connect()返回false。

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值