‌从入门到精通:QThread在Qt中的高效应用‌

‌从入门到精通:QThread在Qt中的高效应用‌一提 Qt 多线程 是不是立马想起 QThread 跟 C 11 的线程比 这哥们儿既有相似的地方 又藏着不少 Qt 独有的骚操作 别的不说 QThread 自带消息循环 就是那个 exec 函数 每个线程都靠它处理自己的那些杂

大家好,欢迎来到IT知识分享网。

一提 “Qt 多线程”,是不是立马想起QThread?

跟 C++11 的线程比,这哥们儿既有相似的地方,又藏着不少 Qt 独有的骚操作!别的不说,QThread 自带消息循环(就是那个 exec () 函数),每个线程都靠它处理自己的那些杂事儿,这一点就很有 Qt 内味儿!

对C++线程池有兴趣的朋友,可以看前篇:手撕线程池:C++程序员的能力试金石

不过有个规矩得先拎清:QCoreApplication::exec () 这货必须在主线程(也就是跑 main () 的线程)里调用。在 GUI 程序里,这主线程也叫 GUI 线程,是唯一能碰界面控件的线程,其他线程都得围着它转。所以想玩 QThread,先把 QApplication(或者 QCoreApplication)对象安排上,没它一切白搭!

为啥非要搞多线程?

你想啊,要是 GUI 程序里有个特耗时的活儿,单线程干的话,窗口直接卡成 PPT,用户点啥都没反应,体验贼差!这时候多线程就派上用场了:主线程专心管界面互动,子线程埋头干那些费劲儿的逻辑运算,各司其职,效率高不说,用户看着也舒坦!

但 Qt 多线程有几个坑必须提前踩明白:

  • 主线程(GUI 线程):管窗口事件、更新控件,地位超然
  • 子线程:只能干后台活儿,碰界面控件就是找死,程序分分钟崩给你看
  • 线程间传数据?老老实实靠 Qt 的信号槽机制,别瞎搞其他路子!

Part1QThread 线程类

Qt 给咱们准备了 QThread 这个线程类(注意哦,它继承自 QObject,跟那个 QRunnable 不是一回事儿),用它就能轻松搞出个子线程。

1.1、核心成员函数(记住这几个就够用)

// 创建线程对象(父对象可选) QThread::QThread(QObject *parent = nullptr); // 判断线程跑完了没? bool isFinished() const; // 正在跑任务吗? bool isRunning() const; // 设置优先级,让线程“卷”起来 or “躺平” void setPriority(Priority p); Priority priority() const;

优先级都有啥?从躺平到拼命三郎:

优先级

说明

IdlePriority

最low,系统闲了才轮到你

NormalPriority

默认,普通人

TimeCriticalPriority

拼命三郎,CPU给我往死里干!

⚠️ 默认是 InheritPriority:子线程跟我爹(创建它的线程)一样卷。

1.2、信号 & 槽(线程间的“暗号”)

[slot] void start();        // 启动线程,run() 开干! [slot] void quit();          // 让线程退出事件循环 [slot] void terminate();     // 强杀线程(慎用!可能炸内存!) [signal] void started();     // 线程启动了,兄弟们注意! [signal] void finished();    // 干完收工,可以清理现场了!

terminate() 是核武器,不到万不得已别用!数据没保存、资源没释放,直接崩给你看!

1.3、静态工具函数(实用小技巧)

[static] QThread* currentThread();        // 我是谁?我在哪? [static] int idealThreadCount();          // 我电脑有几核?咱最多开几个线程合适? [static] void sleep(unsigned long secs);  // 秒级暂停 [static] void msleep(unsigned long msecs); // 毫秒级暂停(常用) [static] void usleep(unsigned long usecs); // 微秒级,精准控时

小技巧:msleep(1) 配合循环,比死循环空转省电多了!

1.4、虚函数:run() —— 线程的“主菜”

virtual void run();

这就是你子线程的入口!你不重写它,start()了也是白搭!

默认实现是调exec(),也就是开启事件循环等信号槽。你要做计算任务?那就重写run(),把活儿写进去!

‌从入门到精通:QThread在Qt中的高效应用‌

专注于Linux C/C++技术讲解~

专注于Linux C/C++技术讲解~

Part2QThread的两种玩法

方法一:派生QThread,重写run()

这是最直接的玩法,步骤简单:

  1. 搞个 MyThread 类继承 QThread
  2. 重写 run () 方法,把要干的活儿全塞进去
  3. 在主线程里 new 个 MyThread 对象,调用 start () 启动

注意事项:

  • 别在外面直接调用 run ()!想启动线程必须用 start (),它会自动去调 run ()
  • 子线程里绝对不能碰界面控件!碰了就等着程序崩溃吧
  • 只有主线程能操作界面,这是铁律

举个栗子: 用多线程处理个 10 秒的耗时操作(按钮一点就启动)

线程类长这样:

// workThread.h class workThread : public QThread { public:     void run(); }; // workThread.cpp workThread::workThread(QObject* parent) {} // 线程入口:后台干活的地方 void workThread::run() {     qDebug() << "当前子线程ID:" << QThread::currentThreadId();     qDebug() << "开始执行线程";     QThread::sleep(10);  // 模拟耗时操作     qDebug() << "线程结束"; }

主线程里这么用:

// Threadtest.h class Threadtest : public QMainWindow {     Q_OBJECT public:     Threadtest(QWidget *parent = Q_NULLPTR); private:     Ui::ThreadtestClass ui;     void btn_clicked();     workThread* thread; }; // threadtest.cpp Threadtest::Threadtest(QWidget* parent) : QMainWindow(parent) {     ui.setupUi(this);     connect(ui.btn_start, &QPushButton::clicked, this, &Threadtest::btn_clicked);     thread = new workThread;  // 在主线程里创建子线程对象 } void Threadtest::btn_clicked()  {       qDebug() << "主线程id:" << QThread::currentThreadId();       thread->start();  // 启动子线程 }

方法二:moveToThread + 信号槽

第一种玩法有个缺点:如果一个子线程要干多个活儿,run () 里的代码就会乱糟糟的,不好维护。所以 Qt 又给了第二种玩法,用信号槽来搞,灵活多了!把要在线程里干的活儿写成槽函数就行。

步骤:

  1. 搞个 MyWork 类继承 QObject,里面写个 working () 函数(就是要在子线程里干的活儿)
  2. 主线程里 new 个 QThread 对象(这就是子线程本体)
  3. 再 new 个 MyWork 对象(千万别给它指定父对象! 不然移不动)
  4. 用 moveToThread () 把 MyWork 对象挪到子线程里
  5. 调用 start () 启动子线程(这时候线程启动了,但 MyWork 还没开始干活)
  6. 触发 MyWork 的 working () 函数(比如用按钮信号连一下),这时候活儿就在子线程里跑了

代码样例:

// MyWork.h class MyWork : public QObject {     Q_OBJECT public:     explicit MyWork(QObject *parent = nullptr);     // 工作函数     void working(); signals:     void curNumber(int num); public slots: }; // mywork.cpp MyWork::MyWork(QObject *parent) : QObject(parent) {} void MyWork::working() {     qDebug() << "当前线程对象的地址: " << QThread::currentThread();     int num = 0;     while(1)     {         emit curNumber(num++);  // 发信号给主线程         if(num == )             break;         QThread::usleep(1);  // 稍微歇口气     }     qDebug() << "run() 执行完毕, 子线程退出..."; } // 主程序 MainWindow::MainWindow(QWidget *parent) :     QMainWindow(parent),     ui(new Ui::MainWindow) {     ui->setupUi(this);     qDebug() << "主线程对象的地址: " << QThread::currentThread();     // 创建线程对象     QThread* sub = new QThread ;     // 创建工作对象(别给父对象!)     MyWork* work = new MyWork;     // 把工作对象挪到子线程里     work->moveToThread(sub);     // 启动线程     sub->start();     // 点击按钮就让work开始干活(信号槽跨线程)     connect(ui->startBtn, &QPushButton::clicked, work, &MyWork::working);     // 子线程发的信号,主线程来更新界面     connect(work, &MyWork::curNumber, this, [=](int num)     {         ui->label->setNum(num);     }); } MainWindow::~MainWindow() {     delete ui; }

注意事项:

  • 这种方式超灵活!多个不相关的活儿可以搞多个 MyWork 类,分别挪到不同线程里,代码清爽得很
  • 子线程碰 UI = 找死!想更界面就得靠信号槽,让主线程去更
  • MyWork 对象别设父对象!不然 moveToThread 会报错:QObject::moveToThread: Cannot move objects with a parent
  • 跨线程信号槽的连接方式有讲究(connect 的第五个参数):
    • 自动连接 (AutoConnection)
    • :默认的,同线程就直接连,不同线程就排队连
    • 直接连接 (DirectConnection)
    • :不管在哪,发信号就立马调槽函数(槽函数在发送者线程跑)
    • 队列连接 (QueuedConnection)
    • :信号先排队,等接收者线程的事件循环处理到了再调槽函数(槽函数在接收者线程跑)
    • 阻塞队列连接 (BlockingQueuedConnection)
    • :跨线程专用,发完信号就等着,槽函数跑完了才继续(容易死锁,慎用)
    • 唯一连接 (UniqueConnection)
    • :配合上面几种用,保证相同信号槽只连一次

Part3线程安全与同步

QThread 继承自 QObject,能发信号告诉别人自己开始或结束了,还有一堆槽函数能用。QObject 在多线程里也能用,发个信号就能在别的线程里调槽函数,还能给其他线程的对象发事件。

3.1 线程同步

3.1.1、基础概念

  • 临界资源:同一时间只能让一个线程碰的东西
  • 线程间互斥:多个线程抢着用临界资源
  • 线程锁:保护临界资源的保镖,一个线程用的时候锁上,用完再开
  • 线程死锁:线程们互相等着对方手里的资源,结果谁都动不了

死锁的条件:

  • 资源不能抢(不可抢占)
  • 线程要多个资源才能干活
  • 手里拿着资源还等着要别人的

Qt 里搞线程同步的家伙有:QMutex、QReadWriteLock、QSemaphore、QWaitCondition。线程就是要能并发跑,但关键时候该等还得等。

3.1.2、互斥锁相关

  • QMutex:最基本的锁,同一时间只让一个线程拿到。要是一个线程拿了锁没放,其他线程就得等着。千万别忘了解锁,不然就死锁了!
  • QMutexLocker:QMutex 的好帮手!在复杂代码里手动 lock () 和 unlock () 容易出错,用这货就省心了 —— 创建的时候自动上锁,出了作用域自动解锁,跟 C++ 的 std::lock_guard 一个意思。
  • QWaitCondition:让线程能等个条件。一个或多个线程可以等着,别的线程用 wakeOne ()(叫醒一个)或 wakeAll ()(全叫醒)来通知它们条件到了,类似 C++ 的 condition_variable。

3.2 QObject 的可重入性问题

  • 线程安全:多个线程同时调函数,就算用共享数据也没事(因为访问是串行的)
  • 可重入:多个线程同时调函数,但每个线程只用自己的数据

简单说:线程安全的函数一定可重入,但可重入的不一定线程安全。

QObject 是可重入的,它的很多非 GUI 子类(比如 QTimer、QTcpSocket 这些)也是。但用的时候有规矩:

  1. QObject 对象必须生在父对象所在的线程里。所以千万别把 QThread 对象当爹传给它线程里的对象(因为 QThread 自己生在别的线程里)。
  2. 事件驱动的对象(比如定时器、网络模块)最好只在一个线程里用。在不属于它的线程里启动定时器、连 socket 都不行。线程里创建的对象,一定要在线程删之前自己先删干净,在 run () 里用栈对象就很方便。
  3. GUI 类(尤其是 QWidget 和它的子类)不可重入,只能在 GUI 线程里用。QCoreApplication::exec () 也得在 GUI 线程里调。

实际开发里,耗时操作放子线程,干完了发个信号让主线程更新界面,完美解决。另外,QApplication 创建之前别搞 QObject,容易崩溃;也别搞 QObject 的静态实例,坑得很。

3.3、线程的事件循环

每个线程都能有自己的事件循环:

  • 主线程用 QCoreApplication::exec () 启动,对话框程序有时候用 QDialog::exec ()
  • 子线程用 QThread::exec () 启动,也有 exit () 和 quit (),记得配合 wait () 用

事件循环的作用大了去了:

  • 能让线程用那些需要事件循环的 Qt 类(比如 QTimer、QTcpSocket)
  • 能让跨线程的信号槽工作(信号发到接收线程的事件循环里,再调槽函数)

QObject 生在哪个线程,就属于哪个线程,事件也会发到那个线程的事件循环里。可以用 thread () 查它属于哪个线程,用 moveToThread () 挪窝(但有父对象的挪不了)。

跨线程删对象别直接用 delete,用 deleteLater () 更安全 —— 它会发个 DeferredDelete 事件,让对象自己的线程去处理删除。

要是线程没跑事件循环,那事件就处理不了。比如子线程里 new 了个 QTimer,却没调 exec (),那 timeout () 信号永远发不出来,deleteLater () 也没用。


QCoreApplication::postEvent () 可以给任何线程的任何对象发事件,会自动传到对象所在线程的事件循环里。线程也支持事件过滤器,但监控者和被监控者得在一个线程里。sendEvent () 只能给当前线程的对象发事件。

总结

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/189409.html

(0)
上一篇 2025-10-01 09:10
下一篇 2025-10-01 09:20

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信