Qt学习日记 Day11:多线程(二)+线程同步/互斥

今天学了什么

今天继续学习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); }

线程同步/互斥

① 同步:

定义:控制线程执行先后顺序,一个线程要等另一个线程通知才继续运行;

作用:保证执行顺序;

  1. QWaitCondition(配套QMutex)

方法:wait() 休眠等待、wakeOne()唤醒一个、wakeAll()唤醒全部;

  1. 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的结果可能会不准确,会存在少加的情况。

复盘补充

  1. extern int num;:只是声明,不分配内存。多个文件include这个头文件,不会新建变量。必须在某一个cpp里写int num=0; 做唯一的定义(多个.cpp共用num)。

  2. 头文件直接写int num=0;:属于定义。只要别的cpp包含这个头,就会生成一份num,链接报LNK2005多重定义。

  3. 共享变量多线程不加锁:num++不是原子操作,会发生数据丢失,最终结果不对、每次运行结果不一样(竞态条件)。

  4. QMutex互斥锁:同一时间只允许一个线程拿到锁,保护共享资源。QMutexLocker自动上锁,出作用域自动解锁。

  5. QWaitCondition:用于线程同步(控制执行顺序),必须搭配QMutex;wait内部自动解锁休眠,唤醒后自动重新上锁,需要传锁地址(&mutex)。

  6. 直接调用obj->func():谁调用,函数就在哪个线程执行,不受moveToThread/QThread影响;只有信号槽触发槽函数才会跟随对象所属线程。

  7. 继承QThread:只有run函数在子线程执行;对象本身归属主线程,直接调用它的普通成员函数跑在主线程。

  8. moveToThread:只是把对象的信号槽队列放到子线程;仅信号触发的槽函数跑子线程,直接调用函数无效。

  9. 线程池start:任务提交顺序 ≠ 实际执行顺序,由操作系统调度。

优雅退出与terminate()强制退出

优雅退出:

  1. 概念:发送停止通知,让子线程主动结束代码、完成资源清理后正常退出,安全可控;

  2. 场景:

① moveToThread:线程自带exec()事件循环;

② 继承QThread重写run,手写while无限循环;

③ 继承QThread重写run,有限for循环(会自己跑完);

  1. 对应退出方式

moveToThread:quit()关闭exec事件循环

继承QThread手写while死循环:原子stop标志位跳出循环(quit()无效)

继承QThread有限for循环:无需quit、标志,循环跑完自动结束

  1. 通用步骤:

1)发停止通知(quit() / 设置stop标志);

2)子线程收到通知,退出循环/退出exec,执行清理;

3)主线程调用wait(),等待子线程完全结束;

terminate() 强制终止:

  1. 概念:操作系统直接强行切断线程,线程立刻停在当前代码位置;

  2. 使用:线程对象->terminate(),一般只做极端兜底,不常规使用;

  3. 缺点:不会执行清理代码;锁、文件、堆资源无法正常释放,容易死锁、内存泄漏、文件损坏;

quit、wait 的作用:

  • quit():只用来退出exec()事件循环;没有exec时,quit无效。
  • wait():和事件循环无关。主线程阻塞,等待子线程执行完毕,所有场景优雅退出基本都搭配wait。

对比:

• 优雅退出:线程主动退出,资源正常释放,稳定安全;根据有没有exec、是不是手写循环选方案。

• terminate:强制杀死线程,立刻停止,无清理流程,风险高,尽量不用。

知识点补充

跨线程信号槽是怎么连接的?

答:AutoConnection 在发送方和接收方位于不同线程时,会自动转为 QueuedConnection。信号

发出后,槽函数的调用会被封装成一个事件,放入接收者所在线程的事件队列,由接收者线程的事

件循环异步取出并执行。这也是为什么子线程不能直接操作UI,但可以通过信号槽安全地通知主线

程更新界面的根本原因。

相关推荐
叼烟扛炮1 小时前
Linux 学习笔记(三十四):线程同步——条件变量与生产消费者模型
linux·运维·服务器·笔记·学习·1024程序员节
宝绿特塑业再生科技1 小时前
尺寸稳定性重构:改性再生HDPE破解户外家具热胀冷缩困局
经验分享·学习·生活·户外家具
无心水1 小时前
5、信息安全技术基础知识 核心学习笔
人工智能·学习·软考高级·架构师·软考·高级架构师·软考架构师
fanged1 小时前
蓝牙学习5(BLE Audio)(TODO)
学习
小陈phd1 小时前
深入理解agent学习笔记(十一)——用户记忆系统
笔记·学习
无心水1 小时前
8、系统架构设计基础知识 核心考点与学习笔记
笔记·学习·系统架构·架构师·软考·软考架构师·软考高级架构师
行思理11 小时前
go语言学习难度及适合学习的人群
开发语言·学习·golang
楚山之石15 小时前
【Medical Image Analysis】深度学习医学图像合成综述:伪CT、合成MRI与合成PET的架构、性能与临床边界
图像处理·人工智能·学习·mr·mri