一个进程可以有一个或更多线程同时运行。线程可以看做是“轻量级进程”,进程完全由操作系统管理,线程即可以由操作系统管理,也可以由应用程序管理。
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,因此必须要求事件循环.
我们经常会遇到一些很耗时的操作,以至于阻塞事件循环,最后导致程序未响应。面对这样的问题,通常有三种解决方案:
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
对象。这个对象可以用来查询当前的运算状态(也就是任务的进度),可以用来暂停/回复/取消任务,当然也可以用来获得运算结果。
每个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的所有子对象都必须和其父对象在同一线程。这意味着:
使用时,一个比较推荐的方式是:将处理任务的部分与管理线程的部分分离。简单来说,我们可以利用一个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(object, "methodName",
Qt::QueuedConnection,
Q_ARG(type1, arg1),
Q_ARG(type2, arg2));
上面函数调用中出现的参数类型都必须提供一个公有构造函数,一个公有的析构函数和一个公有的复制构造函数,并且要使用qRegisterMetaType()函数向 Qt 类型系统注册。
跨线程的信号槽也是类似的。当我们将信号与槽连接起来时,QObject::connect()的最后一个参数将指定连接类型:
Qt::DirectConnection
:直接连接意味着槽函数将在信号发出的线程直接调用Qt::QueuedConnection
:队列连接意味着向接受者所在线程发送一个事件,该线程的事件循环将获得这个事件,然后之后的某个时刻调用槽函数Qt::BlockingQueuedConnection
:阻塞的队列连接就像队列连接,但是发送者线程将会阻塞,直到接受者所在线程的事件循环获得这个事件,槽函数被调用之后,函数才会返回Qt::AutoConnection
:自动连接(默认)意味着如果接受者所在线程就是当前线程,则使用直接连接;否则将使用队列连接注意在上面每种情况中,发送者所在线程都是无关紧要的!在自动连接情况下,Qt 需要查看信号发出的线程是不是与接受者所在线程一致,来决定连接类型。注意,Qt 检查的是信号发出的线程,而不是信号发出的对象所在的线程!
有关线程,你可以做的是:
不应该做的是:
不能做的是:
手机扫一扫
移动阅读更方便
你可能感兴趣的文章