醋醋百科网

Good Luck To You!

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

一提 “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(),把活儿写进去!

专注于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 == 10000000)
            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 () 只能给当前线程的对象发事件。


总结

主线程只管UI,子线程负责干活!
子线程不准碰UI,通信靠信号槽!
任务多?用moveToThread,别把run()写成屎山!
共享资源要加锁,否则数据全炸锅!
对象归属要清楚,deleteLater保平安!

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言