1.QT多线程使用小结
阅读原文时间:2023年07月09日阅读:1

开头

一个进程可以有一个或更多线程同时运行。线程可以看做是“轻量级进程”,进程完全由操作系统管理,线程即可以由操作系统管理,也可以由应用程序管理。

Qt 使用QThread来管理线程。

当我们面临主进程中存在一些非常耗时的操作,会阻塞运行的时候,需要使用线程。

Qt 线程的最基本的方法我想应该是重写run()函数。比如说:

实例一个XXXThread类,继承自QThread类,重写了其run()函数。run()函数就是线程启动后需要执行的代码。

场景举例

我们通过点击按钮在UI线程中触发了一个QThread::start(),来启动线程。线程里会运行run()里的内容。完成后会发出一个信号,告诉UI线程一个/系列操作已经完成了,如果要干点什么就可以动手了。

另外,我们将XXXThread::deleteLater()函数与XXXThread::finished()信号连接起来,当线程完成时,系统可以帮我们清除线程实例。

  Qt 多线程的优势设计使得它使用起来变得容易,但是坑很多,稍不留神就会被绊住,尤其是涉及到与 QObject 交互的情况。

事件循环

此处插播一则WiKi文档 https://wiki.qt.io/Threads_Events_QObjects

  Qt 是事件驱动的。在 Qt 中,事件由一个普通对象表示(QEvent或其子类)。所有QObject的子类都可以通过覆盖QObject::event()函数来控制事件的对象。

  事件并不是一产生就被分发。事件产生之后被加入到一个队列中(这里的队列含义同数据结构中的概念,先进先出),该队列即被称为事件队列。事件分发器遍历事件队列,如果发现事件队列中有事件,那么就把这个事件发送给它的目标对象。这个循环被称作事件循环。

  调用QCoreApplication::exec() 函数意味着进入了主循环。我们把事件循环理解为一个无限循环,直到QCoreApplication::exit()或者QCoreApplication::quit()被调用,事件循环才真正退出。

//事件循环伪代码
while (is_active)
{
while (!event_queue_is_empty) {
dispatch_next_event();
}
wait_for_more_events();
}

  代码里面的while会遍历整个事件队列,发送从队列中找到的事件;wait_for_more_events()函数则会阻塞事件循环,直到又有新的事件产生。

  wait_for_more_events()函数所得到的新的事件都应该是由程序外部产生的。因为所有内部事件都应该在事件队列中处理完毕了。\事件循环在wait_for_more_events()函数进入休眠,并且可以被下面几种情况唤醒:

  • 窗口管理器的动作(键盘、鼠标按键按下、与窗口交互等);

  • 套接字动作(网络传来可读的数据,或者是套接字非阻塞写等);

  • 定时器;

  • 由其它线程发出的事件;

  • 组件的绘制与交互:QWidget::paintEvent()会在发出QPaintEvent事件时被调用。该事件可以通过内部QWidget::update()调用或者窗口管理器(例如显示一个隐藏的窗口)发出。所有交互事件(键盘、鼠标)也是类似的:这些事件都要求有一个事件循环才能发出。

  • 定时器:长话短说,它们会在select(2)或其他类似的调用超时时被发出,因此你需要允许 Qt 通过返回事件循环来实现这些调用。

  • 网络:所有低级网络类(QTcpSocket、QUdpSocket以及QTcpServer等)都是异步的。当你调用read()函数时,它们仅仅返回已可用的数据;当你调用write()函数时,它们仅仅将写入列入计划列表稍后执行。只有返回事件循环的时候,真正的读写才会执行。注意,这些类也有同步函数(以waitFor开头的函数),但是它们并不推荐使用,就是因为它们会阻塞事件循环。高级的类,例如QNetworkAccessManager则根本不提供同步 API,因此必须要求事件循环.

我们经常会遇到一些很耗时的操作,以至于阻塞事件循环,最后导致程序未响应。面对这样的问题,通常有三种解决方案:

  1. 多线程;
  2. 手动进入事件循环运行,重复的去一遍遍地调用QCoreApplication::processEvents()函数。QCoreApplication::processEvents()函数会发出事件队列中的所有事件,并且立即返回到调用者。仔细想一下,我们在这里所做的,就是模拟了一个事件循环。
  3. 使用QEventLoop类重新进入新的事件循环。通过调用QEventLoop::exec()函数,我们重新进入新的事件循环,给QEventLoop::quit()槽函数发送信号则退出这个事件循环。

QThread类常用成员函数&其他线程相关类

void run ();
//线程体函数,需要用户自定义该函数执行的内容,内容里也可以使用exec()实现事件循环
void finished () [signal]
//信号成员函数,表示该线程执行完成,已经在run()函数中return了

void start()[slot]
//启动函数,将会执行run()函数,并且发射信号started()
void started () [signal]
//信号成员函数,表示该线程已启动

void terminate() [slot]
//强制结束正在进行的线程(不推荐,因为不会考虑资源释放), 并且发射信号terminated ()
void QThread::terminated () [signal]
//信号成员函数,表示该线程已停止

bool wait ( unsigned long time = ULONG_MAX );
//阻塞等待线程执行结束,如果time(单位毫秒)时间结束,线程还未结束,则返回false,否则返回true,如果time= ULONG_MAX,则表示一直等待
sleep ( unsigned long secs )、msleep()、usleep()、
//休眠当前线程秒,毫秒,微妙

void quit()
//告诉线程事件循环(前提必须让thread进入exec(),否则无反应)退出,返回0表示成功,相当于调用了QThread::exit(0)。
void setPriority(Priority priority);
//设置正在运行的线程优先级,必须在调用start()启动线程之后设置才有用
bool isFinished() const
//线程是否结束
bool isRunning() const
//线程是否正在运行

  这是一个轻量级的抽象类,用于开始一个另外线程的任务。这种任务是运行过后就丢弃的。由于这个类是抽象类,我们需要继承,然后重写其纯虚函数QRunnable::run()。

这里要提到QThreadPool类。,这个类用于管理一个线程池。通过调用QThreadPool::start(runnable)函数,我们将一个对象放入线程池的执行队列。一旦有线程可用,线程池将会选择一个QRunnable对象,然后在那个线程开始执行。所有 Qt 应用程序都有一个全局线程池,我们可以使用QThreadPool::globalInstance()获得这个全局线程池;或者,我们也可以自己创建私有的线程池,并进行手动管理。

  QRunnable不是一个QObject,因此也就没有内建的与其它组件交互的机制。为了与其它组件进行交互,你必须自己编写低级线程原语,例如使用 mutex 守护来获取结果等。

  是一个高级 API,构建于QThreadPool之上,用于处理大多数通用的并行计算模式:map、reduce 以及 filter。它还提供了QtConcurrent::run()函数,用于在另外的线程运行一个函数。注意,QtConcurrent是一个命名空间而不是一个类,因此其中的所有函数都是命名空间内的全局函数。

  QtConcurrent不要求我们使用低级同步原语:所有的QtConcurrent都返回一个QFuture对象。这个对象可以用来查询当前的运算状态(也就是任务的进度),可以用来暂停/回复/取消任务,当然也可以用来获得运算结果。

QObject

  每个QT应用程序中,至少有一个事件循环,在这个循环里调用了QCoreApplication::exec()。此外,QThread也可以开启其他的事件循环,但只限于线程内部。由此我们区分出主进程和线程。主进程也叫 GUI 线程,因为所有有关 GUI 的操作都必须在这个线程进行。

  QThread的局部事件循环则可以通过在QThread::run()中调用QThread::exec()开启。

class Thread : public QThread
{
protected:
void run() {
/* …… */
exec();
}
};

  Qt 4.4 版本以后,run()不再是纯虚函数,它会调用exec() 函数,有quit()和exit()函数来终止事件循环。

  线程的事件循环用于为线程中的所有QObjects对象分发事件;默认情况下,这些对象包括线程中创建的所有对象,或者是在别处创建完成后被移动到该线程的对象。我们说,一个QObject的所依附的线程是指它所在的那个线程。它同样适用于在QThread的构造函数中构建的对象。然而要注意,在线程的构造函数是在主进程中执行的,于是构造函数中构建的对象所依附的线程实际上是主进程。

  我们可以通过调用QObject::thread()可以查询一个QObject的线程依附性。

QObject的线程依附性是可以改变的,方法是调用QObject::moveToThread()函数,来迁移一个对象及其所有子对象的线程依附性。由于QObject不是线程安全的,所以我们只能在该对象所在线程上调用这个函数。也就是说,我们只能在对象所在线程将这个对象移动到另外的线程,不能在另外的线程改变对象的线程依附性。还有一点是,Qt 要求QObject的所有子对象都必须和其父对象在同一线程。这意味着:

  • 不能对有父对象(parent 属性)的对象使用QObject::moveToThread()函数
  • 不能在QThread中以这个QThread本身作为父对象创建对象,例如下面的代码片段:这是因为QThread对象所依附的线程是创建它的那个线程,而不是它所代表的线程。

使用时,一个比较推荐的方式是:将处理任务的部分与管理线程的部分分离。简单来说,我们可以利用一个QObject的子类,使用QObject::moveToThread()改变其线程依附性。

class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork() {
/* … */
}
};

/* … */
QThread *thread = new QThread;
Worker *worker = new Worker;
connect(obj, SIGNAL(workReady()), worker, SLOT(doWork()));
worker->moveToThread(thread);
thread->start();

  Qt的线程类在销毁实例对象之前,线程中的对象必须要被delete,于是我们应该在run()函数栈上创建对象。于是面临一个问题:线程之间怎么进行通信?

  Qt 提供了一个优雅清晰的解决方案:我们在线程的事件队列中加入一个事件,然后在事件处理函数中调用我们所关心的函数。显然这需要线程有一个事件循环。这种机制依赖于 moc 提供的反射。

  信号、槽和使用Q_INVOKABLE宏标记的函数可以在另外的线程中调用。 

  使用Q_INVOKABLE来修饰成员函数,目的在于被修饰的成员函数能够被元对象系统所唤起

  Q_INVOKABLE与QMetaObject::invokeMethod均由元对象系统唤起。这一机制在Qt C++/QML混合编程,跨线程编程,Qt Service Framework 以及 Qt/ HTML5混合编程以及里广泛使用。

  QMetaObject::invokeMethod

QMetaObject::invokeMethod()静态函数会这样调用:  

QMetaObject::invokeMethod(object, "methodName",
Qt::QueuedConnection,
Q_ARG(type1, arg1),
Q_ARG(type2, arg2));

  上面函数调用中出现的参数类型都必须提供一个公有构造函数,一个公有的析构函数和一个公有的复制构造函数,并且要使用qRegisterMetaType()函数向 Qt 类型系统注册。

  SIGNAL&SLOT

  跨线程的信号槽也是类似的。当我们将信号与槽连接起来时,QObject::connect()的最后一个参数将指定连接类型:

  • Qt::DirectConnection:直接连接意味着槽函数将在信号发出的线程直接调用
  • Qt::QueuedConnection:队列连接意味着向接受者所在线程发送一个事件,该线程的事件循环将获得这个事件,然后之后的某个时刻调用槽函数
  • Qt::BlockingQueuedConnection:阻塞的队列连接就像队列连接,但是发送者线程将会阻塞,直到接受者所在线程的事件循环获得这个事件,槽函数被调用之后,函数才会返回
  • Qt::AutoConnection:自动连接(默认)意味着如果接受者所在线程就是当前线程,则使用直接连接;否则将使用队列连接

注意在上面每种情况中,发送者所在线程都是无关紧要的!在自动连接情况下,Qt 需要查看信号发出的线程是不是与接受者所在线程一致,来决定连接类型。注意,Qt 检查的是信号发出的线程,而不是信号发出的对象所在的线程!

TO DO & WARNING & CANNOT

有关线程,你可以做的是:

  • 在QThread子类添加信号。这是绝对安全的,并且也是正确的(前面我们已经详细介绍过,发送者的线程依附性没有关系)

不应该做的是:

  • 调用moveToThread(this)函数
  • 指定连接类型:这通常意味着你正在做错误的事情,比如将QThread控制接口与业务逻辑混杂在了一起(而这应该放在该线程的一个独立对象中)
  • 在QThread子类添加槽函数:这意味着它们将在错误的线程被调用,也就是QThread对象所在线程,而不是QThread对象管理的线程。这又需要你指定连接类型或者调用moveToThread(this)函数
  • 使用QThread::terminate()函数

不能做的是:

  • 在线程还在运行时退出程序。使用QThread::wait()函数等待线程结束
  • 在QThread对象所管理的线程仍在运行时就销毁该对象。如果你需要某种“自行销毁”的操作,你可以把finished()信号同deleteLater()槽连接起来

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章