一提 “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()
这是最直接的玩法,步骤简单:
- 搞个 MyThread 类继承 QThread
- 重写 run () 方法,把要干的活儿全塞进去
- 在主线程里 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 又给了第二种玩法,用信号槽来搞,灵活多了!把要在线程里干的活儿写成槽函数就行。
步骤:
- 搞个 MyWork 类继承 QObject,里面写个 working () 函数(就是要在子线程里干的活儿)
- 主线程里 new 个 QThread 对象(这就是子线程本体)
- 再 new 个 MyWork 对象(千万别给它指定父对象! 不然移不动)
- 用 moveToThread () 把 MyWork 对象挪到子线程里
- 调用 start () 启动子线程(这时候线程启动了,但 MyWork 还没开始干活)
- 触发 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 这些)也是。但用的时候有规矩:
- QObject 对象必须生在父对象所在的线程里。所以千万别把 QThread 对象当爹传给它线程里的对象(因为 QThread 自己生在别的线程里)。
- 事件驱动的对象(比如定时器、网络模块)最好只在一个线程里用。在不属于它的线程里启动定时器、连 socket 都不行。线程里创建的对象,一定要在线程删之前自己先删干净,在 run () 里用栈对象就很方便。
- 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保平安!