【QT八股文】系列之篇章3 | QT的多线程
- 前言
- 4. 多线程
- 为什么需要使用线程池
- 线程池的基础知识
- python中创建线程池的方法
- 使用threading库+队列Queue来实现线程池
- 使用threadpool模块,这是个python的第三方模块,支持python2和python3
- QThread的定义
- QT多线程知识点
- 怎么做多线程(原理篇)
- 方案1:信号与线程
- 方案2:线程
- 方案3:线程与队列
- QT多线程的使用方法(具体方法篇)
- QT 多线程/QT线程同步的方法
- 5. QThread与QObject
- QThread的定义
- 对QObject的理解
- Q_OBJECT的作用是什么,内部实现了些什么
- QObject是否是线程安全的/线程依附性是否可以改变/如何安全调用
- 下一章笔记
- 说明
前言
- 第一篇章主要是基础定义及QT中重要的事件机制
笔记链接:【QT八股文】系列之篇章1 | QT的基础知识及事件/机制 - 第二篇章主要是QT的信号与槽以及通讯流程
笔记链接:【QT八股文】系列之篇章2 | QT的信号与槽及通讯流程
这里我们主要件点更实际的,也就是多线程以及QThread与QObject
因为介绍到信号与槽,所以笔者我会讲通讯流程提前在前面来介绍
原创文章,未经同意请勿转载
4. 多线程
为什么需要使用线程池
- 减少系统开销:频繁创建/销毁线程的开销大,影响处理效率。而在线程池缓存线程可用已有的闲置线程来执行新任务,避免了创建/销毁带来的系统开销。
- 避免阻塞问题:线程并发数量过多,抢占系统资源从而导致阻塞。线程能共享系统资源,如果同时执行的线程过多,就有可能导致系统资源不足而产生阻塞的情况。
- 管理和控制线程:运用线程池可以进行多线程的调度,有利于延迟执行,优先级执行和定时循环执行策略
线程池的基础知识
这里必须要知道线程池实现主要依靠两个部分,一个是任务队列,另外一个是线程的管理控制中心
很明显。任务队列的数据结构就是队列,先进先出,用Queue模块实现,那先了解一下Queue:
- Queue的常用方法
- Queue.qsize():返回queue的大小。
- Queue.empty():判断队列是否为空,通常不太靠谱。
- Queue.full():判断是否满了。
- Queue.put(item, block=True, timeout=None): 往队列里放数据。
- Queue.put_nowait(item):往队列里存放元素,不等待
- Queue.get(item, block=True, timeout=None): 从队列里取数据。
- Queue.get_nowait(item):从队列里取元素,不等待
- Queue.task_done():表示队列中某个元素是否的使用情况,使用结束会发送信息。
- Queue.join():一直阻塞直到队列中的所有元素都执行完毕。
python中创建线程池的方法
这里必须要知道线程池实现主要依靠两个部分,一个是任务队列,另外一个是线程的管理控制中心
- 使用threading库+队列Queue来实现线程池
- 使用threadpool模块,这是个python的第三方模块,支持python2和python3
- 使用concurrent.futures模块,这个模块是python3中自带的模块,python2.7以上版本也可以安装使用
使用threading库+队列Queue来实现线程池
1、创建一个 Queue.Queue() 的实例,然后使用数据对它进行填充。
2、将经过填充数据的实例传递给线程类,后者是通过继承threading.Thread 的方式创建的。
3、生成守护线程池。
4、每次从队列中取出一个项目,并使用该线程中的数据和 run 方法以执行相应的工作。
5、在完成这项工作之后,使用 queue.task_done() 函数向任务已经完成的队列发送一个信号。
6、对队列执行 join 操作,实际上意味着等到队列为空,再退出主程序。
在使用这个模式时需要注意一点:通过将守护线程设置为 true,程序运行完自动退出。好处是在退出之前,可以对队列执行 join 操作、或者等到队列为空。
-
代码
import Queue import threading import time queue = Queue.Queue() class ThreadNum(threading.Thread): def __init__(self, queue): threading.Thread.__init__(self) self.queue = queue def run(self): while True: #消费者端,从队列中获取num num = self.queue.get() print("Retrieved", num) time.sleep(1) #在完成这项工作之后,使用 queue.task_done() 函数向任务已 经完成的队列发送一个信号 self.queue.task_done() print("Consumer Finished") def main(): #产生一个 threads pool, 并把消息传递给thread函数进行处理,这 里开启10个并发 for i in range(5): t = ThreadNum(queue) t.setDaemon(True) t.start() #往队列中填数据 for num in range(10): queue.put(num) #wait on the queue until everything has been processed queue.join() if __name__ == '__main__': main() time.sleep(500) 输出为: ('Retrieved', 0) ('Retrieved', 1) ('Retrieved', 2) ('Retrieved', 3) ('Retrieved', 4) ('Retrieved', 5) ('Retrieved', 6) ('Retrieved', 7) ('Retrieved', 8) ('Retrieved', 9)
注意运行main函数后继续执行time.sleep(500),可以观察到主线程未结束的情况下ThreadNum(queue)生成的线程还在运行。如果需要停止线程的话可以对以上代码加以修改
-
代码
import Queue import threading import time queue = Queue.Queue() class ThreadNum(threading.Thread): """没打印一个数字等待1秒,并发打印10个数字需要多少秒?""" def __init__(self, queue): threading.Thread.__init__(self) self.queue = queue def run(self): done = False while not done: #消费者端,从队列中获取num num = self.queue.get() if num is None: done = True else: print("Retrieved", num) time.sleep(1) #在完成这项工作之后,使用 queue.task_done() 函数向任务已 经完成的队列发送一个信号 self.queue.task_done() print("Consumer Finished") def main(): #产生一个 threads pool, 并把消息传递给thread函数进行处理,这 里开启10个并发 for i in range(5): t = ThreadNum(queue) t.setDaemon(True) t.start() #往队列中填错数据 for num in range(10): queue.put(num) queue.join() time.sleep(100) for i in range(10): queue.put(None) print('None') time.sleep(200) if __name__ == '__main__': start = time.time() main() print"Elapsed Time: %s" % (time.time() - start)
main函数执行完后队列向线程发送None消息,触发线程的停止标识,这样就可以动态管理线程池了。
使用threadpool模块,这是个python的第三方模块,支持python2和python3
QThread的定义
QThread 是 QT 中用于创建线程的类,它提供了一组方法用于启动、停止、监测线程的运行状态,以及获取线程的相关信息。
QThread 类包含了多个方法,用于启动、停止、监测线程的运行状态,以及获取线程的相关信息。其中:
run()
方法启动线程的执行stop()
方法停止线程的执行join()
方法等待线程的执行完毕detach()
方法将线程从事件循环中移除。
此外,setName()
、setId()
、setPriority()
、getPriority()
、setSignalsBlocked()
、isSignalsBlocked()
等方法用于修改线程的属性。
QT多线程知识点
多线程运行机制:当启动多线程后,注册信号,槽函数为主线程中的函数,当任务完成后,发射信号,在主线程中对UI进行更新。【QThred】
怎么做多线程(原理篇)
方案1:信号与线程
程序启动,创建一个线程(存活周期:直到软件关闭),当点击事件发生,发送信号,该信号连接两个槽,A负责界面变化切换,B进行后台通讯,B通讯结束,再通过信号将结果返回到界面切换,通过这种机制实现界面与通信的分离。
💡 结论:
经过代码测试,发现这个方案并非是异步的,而是同步的,原因是同时连接两个槽,这个槽机制应该是一个列表,串行执行的,必然两个槽的执行会存在先后问题,当一个阻塞,另一个也就阻塞了。
方案2:线程
程序启动,当点击事件发生,发送信号,该信号连接一个槽,槽负责界面变化切换,同时创建一个线程(存活周期:报文发送接收完成既关闭),线程进行后台通讯,通讯结束,再通过信号将结果返回到界面切换,通过这种机制实现界面与通信的分离。线程中发送的数据要通过线程创建时传入。
💡 结论:
该方法虽然实现了界面切换与通讯的异步处理,但是每点击一次按钮,都需要一次线程的创建,而且对于一直保持通讯的心跳机制,还需要单独起一个线程,可谓是花费巨大,感觉不是很好的方法
方案3:线程与队列
为了解决方案2中频繁创建线程的问题,现在做如下改进,程序启动,创建一个线程(存活周期:直到软件关闭),在线程中创建多个队列,线程监控队列,队列分别有信号队列,信息发送队列,当前界面位置队列,当界面事件发生,去修改队列,线程则监控队列,取出队列进行处理,处理之后将结果返回
💡 结论:
这样的处理机制避免了线程的频繁创建,同时能存储一些全局的重要信息,也实现了异步的效果。
QT多线程的使用方法(具体方法篇)
-
方法一:利用python的threading库实现(主要使用threading.Thread类)
- 线程启动使用start()函数
- 如果需要等待线程执行使用join,这样主线程会阻塞
💡 使用join方法会让主线程阻塞在这里,等待子线程结束,在里面可以设置阻塞的时间
-
方法二:继承QThread,并重写run函数
使用QThread类来创建和管理多线程。具体步骤包括:继承QThread类并重写其run()函数,将需要在子线程中执行的代码放入run()函数中【保证线程安全】;在主线程中创建QThread对象,将其指针作为参数传递给需要在子线程中执行的对象;调用QThread对象的start()函数来启动子线程。在线程任务执行过程中,可以使用 QThread 的 join() 方法等待线程执行完毕。① 创建一个类从QThread类派生
② 在子线程类中重写 run 函数, 将处理操作写入该函数中
③ 在主线程中创建子线程对象, 启动子线程, 调用start()函数
💡 需要注意的事项:如果是while循环,想要结束线程,调用QThread::quit是没有用,因为这样的线程根本就不需要事件循环,比较好的方法就是把while内的控制变量设置为false或者直接使用Qt很不推荐的方法QThread::terminate。terminate()强制退出。
-
方法三:使用线程池
QtConcurrent运行一个线程池,它是一个更高级别的API,不适合运行大量的阻塞操作;如果你做了很多阻塞操作,你很快就会耗尽池并让其他请求排队,在那种情况下,QThread(较低级别的构造)可能更适合于操作(每个代表一个线程)。
-
方法四:利用QRunnable 类
QRunnable 类是 PyQt5 中的可运行对象类,它提供了 run() 方法来执行线程,并可以通过 QRunnableInterface 实现线程通信。QRunnable 类创建线程的基本原理是创建一个 QRunnable 实例,并将其作为参数传递给 QApplication 的 thread() 方法创建线程。在线程任务执行过程中,可以使用 QRunnable 的 run() 方法执行线程任务。QRunnable 对象可以访问主线程的 QCoreApplication 对象。在 QRunnable 对象中,需要使用 start() 方法启动线程,并使用 join() 方法等待线程执行完毕。
-
方法五:继承QObject,并将对象移动至子线程(&QThread)
① 将业务处理抽象成一个业务类, 在该类中创建一个业务处理函数
② 在主线程中创建一QThread类对象
③ 在主线程中创建一个业务类对象
④ 将业务类对象移动到子线程中
⑤ 在主线程中启动子线程
⑥ 通过信号槽的方式, 执行业务类中的业务处理函数
💡 多线程使用注意事项:
-
业务对象, 构造的时候不能指定父对象
-
子线程中不能处理ui窗口(ui相关的类)
-
子线程中只能处理一些数据相关的操作, 不能涉及窗口
-
QT 多线程/QT线程同步的方法
- 使用 QMutex 对象:QMutex 是 QT 中用于线程同步的同步原语。每个线程都可以访问一个 QMutex 对象,通过 lock() 和 unlock() 方法实现线程同步。当一个线程需要访问共享资源时,它会首先尝试获取 QMutex 对象的锁,如果锁已经被其他线程获取了,那么该线程将被阻塞,直到锁被释放。
- 使用 QSemaphore 对象:QSemaphore 是 QT 中用于线程同步的同步原语。每个线程都可以访问一个 QSemaphore 对象,通过 semaphore.wait() 和 semaphore.signal() 方法实现线程同步。当一个线程需要访问共享资源时,它会首先尝试等待 QSemaphore 对象的许可,如果许可已经被其他线程获取了,那么该线程将被阻塞。
- 使用 QWaitCondition 对象:QWaitCondition 是 QT 中用于线程同步的同步原语。它结合了 QMutex 和 QSemaphore 的特点,可以更方便地实现线程同步。QWaitCondition 对象包含一个互斥锁和一个信号槽,当一个线程需要等待条件满足时,它会挂起并等待互斥锁的释放,当条件满足时,该线程会被唤醒并执行相应的操作。
- 使用 QEventLoop 对象:QEventLoop 是 QT 中用于处理事件循环的类,它可以实现线程同步。每个线程都可以创建一个 QEventLoop 对象,当线程需要访问共享资源时,它会进入 QEventLoop 对象的 eventLoop() 方法,等待事件处理完毕再继续执行。
- QReadWriteLock类
》一个线程试图对一个加了读锁的互斥量进行上读锁,允许;
》一个线程试图对一个加了读锁的互斥量进行上写锁,阻塞;
》一个线程试图对一个加了写锁的互斥量进行上读锁,阻塞;、
》一个线程试图对一个加了写锁的互斥量进行上写锁,阻塞。
读写锁比较适用的情况是:需要多次对共享的数据进行读操作的阅读线程。
QReadWriterLock 与QMutex相似,除了它对 “read”,"write"访问进行区别对待。它使得多个读者可以共时访问数据。使用QReadWriteLock而不是QMutex,可以使得多线程程序更具有并发性。 - 信号量QSemaphore
但是还有些互斥量(资源)的数量并不止一个,比如一个电脑安装了2个打印机,我已经申请了一个,但是我不能霸占这两个,你来访问的时候如果发现还有空闲的仍然可以申请到的。于是这个互斥量可以分为两部分,已使用和未使用。 - QReadLocker便利类和QWriteLocker便利类对QReadWriteLock进行加解锁
5. QThread与QObject
QThread的定义
QThread 是 QT 中用于创建线程的类,它提供了一组方法用于启动、停止、监测线程的运行状态,以及获取线程的相关信息。
QThread 类包含了多个方法,用于启动、停止、监测线程的运行状态,以及获取线程的相关信息。其中:
run()
方法启动线程的执行stop()
方法停止线程的执行join()
方法等待线程的执行完毕detach()
方法将线程从事件循环中移除。
此外,setName()
、setId()
、setPriority()
、getPriority()
、setSignalsBlocked()
、isSignalsBlocked()
等方法用于修改线程的属性。
对QObject的理解
- Q_OBJECT 是 Qt 框架中的一个宏定义,用于在类的声明中标记该类需要使用 Qt 的元对象系统(Meta-Object System)。使用 Q_OBJECT 宏定义后,编译器会在编译期自动生成元对象代码,包括信号(signal)和槽(slot)的注册、元对象信息的注册等等。
- QObject 类是Qt 所有类的基类。
- QObject是Qt对象模型的核心。这个模型的中心要素就是一种强大的叫做信号与槽无缝对象沟通机制。你可以用 connect() 函数来把一个信号连接到槽,也可以用disconnect() 函数来破坏这个连接。为了避免永无止境的通知循环,你可以用blockSignal() 函数来暂时阻塞信号。保护函数 connectNotify() 和 disconnectNotify() 可以用来跟踪连接。
对象树都是通过QObject 组织起来的,当以一个对象作为父类创建一个新的对象时,这个新对象会被自动加入到父类的 children() 队列中。这个父类有子类的所有权。能够在父类的析构函数中自动删除子类。可以通过findChild()和findChildren() 函数来寻找子类。
每个对象都一个对象名称objectName() ,而且它的类名也可以通过metaObject()函数。你可以通过inherits() 函数来决定一个类是否继承其他的类。当一个对象被删除时,它会发射destory() 信号,你可以抓住这个信号避免某些事情。
对象可以通过event() 函数来接收事情以及过滤来自其他对象的事件。就好比installEventFiter() 函数和eventFilter() 函数。childEvent() 函数能够重载实现子对象的事件。
QObject还提供了基本的时间支持,QTimer类 提高了更高层次的时间支持。
任何对象要实现信号与槽机制,Q_OBJECT 宏都是强制的。你也需要在源原件上运行元对象编译器。不管是否真正用到信号与槽机制,最好在所有QObject子类使用Q_OBJECT宏,以避免出现一些不必要的错误。
所有的Qt widgets 都是基础QObject。如果一个对象是widget,那么isWidgetType()函数就能判断出。
Q_OBJECT的作用是什么,内部实现了些什么
Q_OBJECT 是 Qt 框架中的一个宏定义,用于在类的声明中标记该类需要使用 Qt 的元对象系统(Meta-Object System)。使用 Q_OBJECT 宏定义后,编译器会在编译期自动生成元对象代码,包括信号(signal)和槽(slot)的注册、元对象信息的注册等等。
具体来说,使用 Q_OBJECT 宏定义后,编译器会为该类生成一个 QMetaObject 对象,该对象包含了该类的元对象信息,包括类名、信号和槽的名称、参数类型等等。这些信息可以通过 QObject::metaObject() 函数获取到。
此外,使用 Q_OBJECT 宏定义后,还可以在该类中使用信号和槽,使用 emit 关键字来发射信号,使用 connect 函数将信号和槽连接起来。这些功能都是通过 Qt 的元对象系统实现的。
需要注意的是,使用 Q_OBJECT 宏定义的类必须直接或间接继承自 QObject 类。
- 实现原理
Q_OBJECT 宏定义会为该类自动添加一些成员变量和成员函数,用于支持 Qt 的元对象系统。
1、 QObject 类的虚函数 metaObject(),它返回一个描述该对象的元对象。
2、QMetaObject 类型的静态变量,用于存储该对象的元对象。
QObject是否是线程安全的/线程依附性是否可以改变/如何安全调用
- QObject及其所有子类都不是线程安全的(但都是可重入的)。因此,你不能有两个线程同时访问一个QObject对象,除非这个对象的内部数据都已经很好地序列化(例如为每个数据访问加锁)。
- 可以改变QObject的线程依附性。 调用QObject::moveToThread()函数。该函数会改变一个对象及其所有子对象的线程依附性。
- 如何安全的在另外一个线程中调用QObject对象的接口
- 多线程机制设计
- 将事件提交到接收对象所在线程的事件循环;当事件发出时,响应函数就会被调用。
下一章笔记
下篇笔记链接:【QT的性能优化及异常处理】
下篇笔记主要内容:【待更新】
说明
码字不易,可能当中存在某些字体错误(笔者我没有发现),如果有错误,欢迎大家指正。🤗
另外因为笔记是之前做的,这里我只把我之前做的搬移和重新排版过来,如果有知识上的错误也欢迎大家指正。