今天学了什么
今天继续学习Qt中的多线程(moveToThread和线程池创建多线程)和线程同步/互斥。
继承QObject创建多线程
QThread *t1=new QThread; //创建子线程
gen->moveToThread(t1);
t1->start();
moveToThread对比Qthread优点:
继承 QThread 时,只有 run() 函数内的代码在子线程,该类的其他槽函数仍然在创建线程(通常
是主线程)执行;
而 moveToThread 能让整个 QObject 对象(及其所有槽函数) 都运行在目标线程中,更适合处理
需要持续响应信号的复杂业务逻辑。
线程池
QThreadPool::globalInstance()->start(gen);
Generate::Generate(QObject *parent): QObject{parent} , QRunnable()
{ setAutoDelete(true); }
线程同步/互斥
① 同步:
定义:控制线程执行先后顺序,一个线程要等另一个线程通知才继续运行;
作用:保证执行顺序;
- QWaitCondition(配套QMutex)
方法:wait() 休眠等待、wakeOne()唤醒一个、wakeAll()唤醒全部;
- QSemaphore信号量(自带锁,不用额外mutex)
方法:acquire()拿资源(资源不足就阻塞)、release()释放资源;
② 互斥
定义:多个线程访问同一个共享资源时,同一时间只允许一个线程访问;
作用:防止同时修改数据,避免数据错乱;
类:QMutex
方法:lock() 加锁、unlock() 解锁;QMutexLocker自动管理锁。
总结:
- 互斥:抢资源,不能同时碰;
- 同步:讲顺序,做完再叫下一个;
例:两个子线程同时执行num自加运算:
cpp
#include "mythread.h"
int num=0;
QMutex mutex;
myThread::myThread(QObject *parent)
: QThread{parent}
{}
void myThread::run()
{
qDebug()<<"myThread线程地址"<<QThread::currentThread();
for(int i=0;i<100000;i++)
{
QMutexLocker loker(&mutex);
num++;
}
qDebug()<<"myThread num="<<num;
emit finish(num);
}
myThread2::myThread2(QObject *parent)
: QThread{parent}
{}
void myThread2::run()
{
qDebug()<<"myThread2 线程地址"<<QThread::currentThread();
for(int i=0;i<1000000;i++)
{ QMutexLocker loker(&mutex);
num++;
}
qDebug()<<"myThread2 num="<<num;
emit finish(num);
}
注:如果不加锁,num的结果可能会不准确,会存在少加的情况。
复盘补充
-
extern int num;:只是声明,不分配内存。多个文件include这个头文件,不会新建变量。必须在某一个cpp里写int num=0; 做唯一的定义(多个.cpp共用num)。
-
头文件直接写int num=0;:属于定义。只要别的cpp包含这个头,就会生成一份num,链接报LNK2005多重定义。
-
共享变量多线程不加锁:num++不是原子操作,会发生数据丢失,最终结果不对、每次运行结果不一样(竞态条件)。
-
QMutex互斥锁:同一时间只允许一个线程拿到锁,保护共享资源。QMutexLocker自动上锁,出作用域自动解锁。
-
QWaitCondition:用于线程同步(控制执行顺序),必须搭配QMutex;wait内部自动解锁休眠,唤醒后自动重新上锁,需要传锁地址(&mutex)。
-
直接调用obj->func():谁调用,函数就在哪个线程执行,不受moveToThread/QThread影响;只有信号槽触发槽函数才会跟随对象所属线程。
-
继承QThread:只有run函数在子线程执行;对象本身归属主线程,直接调用它的普通成员函数跑在主线程。
-
moveToThread:只是把对象的信号槽队列放到子线程;仅信号触发的槽函数跑子线程,直接调用函数无效。
-
线程池start:任务提交顺序 ≠ 实际执行顺序,由操作系统调度。
优雅退出与terminate()强制退出
优雅退出:
-
概念:发送停止通知,让子线程主动结束代码、完成资源清理后正常退出,安全可控;
-
场景:
① moveToThread:线程自带exec()事件循环;
② 继承QThread重写run,手写while无限循环;
③ 继承QThread重写run,有限for循环(会自己跑完);
- 对应退出方式
moveToThread:quit()关闭exec事件循环
继承QThread手写while死循环:原子stop标志位跳出循环(quit()无效)
继承QThread有限for循环:无需quit、标志,循环跑完自动结束
- 通用步骤:
1)发停止通知(quit() / 设置stop标志);
2)子线程收到通知,退出循环/退出exec,执行清理;
3)主线程调用wait(),等待子线程完全结束;
terminate() 强制终止:
-
概念:操作系统直接强行切断线程,线程立刻停在当前代码位置;
-
使用:线程对象->terminate(),一般只做极端兜底,不常规使用;
-
缺点:不会执行清理代码;锁、文件、堆资源无法正常释放,容易死锁、内存泄漏、文件损坏;
quit、wait 的作用:
- quit():只用来退出exec()事件循环;没有exec时,quit无效。
- wait():和事件循环无关。主线程阻塞,等待子线程执行完毕,所有场景优雅退出基本都搭配wait。
对比:
• 优雅退出:线程主动退出,资源正常释放,稳定安全;根据有没有exec、是不是手写循环选方案。
• terminate:强制杀死线程,立刻停止,无清理流程,风险高,尽量不用。
知识点补充
跨线程信号槽是怎么连接的?
答:AutoConnection 在发送方和接收方位于不同线程时,会自动转为 QueuedConnection。信号
发出后,槽函数的调用会被封装成一个事件,放入接收者所在线程的事件队列,由接收者线程的事
件循环异步取出并执行。这也是为什么子线程不能直接操作UI,但可以通过信号槽安全地通知主线
程更新界面的根本原因。