无锁并发与高性能数据结构工程 - 第1节
1. Qt多线程入门
说实话,我刚接触多线程那会儿,也被进程和线程这两个概念绕晕过。后来我师父跟我说了一句话,我到现在还记得:「进程是资源分配的最小单位,线程是CPU调度的最小单位」。嗯,这句话很官方,我换个说法给你解释。
你想象一下,你的电脑是一个大工厂。每个进程就是一条独立的生产线——比如Word是一条线,浏览器是另一条线。每条生产线都有自己的厂房、设备和原材料(内存、文件句柄等)。而线程呢,就是这条生产线上的工人。一条生产线可以有一个工人在干活,也可以有好几个工人一起干。
我在项目中遇到过一个问题:一个视频处理程序,单线程跑的时候,界面卡得跟幻灯片似的。为什么?因为主线程既要处理用户点击,又要做视频解码,一个人干两份活,不卡才怪。
核心区别一句话总结:
- 进程之间互相独立,一个崩了不影响另一个
- 线程之间共享进程的资源,一个崩了可能整个进程都挂
- 创建进程的开销大,创建线程的开销小得多
二、Qt多线程的应用场景:什么时候该用?
你可能会问:「我写个简单的计算器,需要多线程吗?」我的回答是:别没事找事。多线程不是银弹,用不好反而引入一堆bug。
那什么时候该用呢?我总结了三个典型场景:
场景1:界面不卡顿
这是最常见的需求。你点了个按钮,程序开始做耗时操作——比如下载文件、处理图片、查询数据库。如果这些操作放在主线程,界面就会「假死」。用户会以为程序崩溃了,然后直接关掉窗口。
我个人的习惯是:任何超过100毫秒的操作,都扔到子线程去。100毫秒是什么概念?就是用户能感知到的延迟阈值。
场景2:并发计算
比如你要处理1000张图片,每张图片做滤镜、缩放、水印。单线程一张一张处理,用户得等到天荒地老。这时候开4个线程,每个处理250张,速度直接翻倍(当然,不是线性翻倍,要考虑CPU核心数)。
场景3:I/O密集型任务
读写文件、网络通信、数据库操作。这些操作大部分时间都在等待,CPU其实很闲。多线程可以让CPU在等待的时候干别的活。
我的避坑建议: 我曾经在一个项目里开了20个线程去读数据库,结果数据库连接池不够用,程序直接崩溃。后来我改用线程池,限制最大线程数为CPU核心数+1,问题就解决了。所以,线程不是越多越好。
三、QThread类的核心概念:你必须要懂的
Qt里的多线程,核心就是QThread类。但很多人一开始就理解错了——包括我。我记得我第一次用QThread,直接在子类里重写了run()方法,然后调用start(),以为这就完事了。结果信号槽死活不触发,查了半天才发现问题。
3.1 两种使用方式
QThread有两种用法,我分别说一下:
方式一:继承QThread(不推荐)
class WorkerThread : public QThread
{
Q_OBJECT
protected:
void run() override {
// 耗时操作
for (int i = 0; i < 100; ++i) {
emit progress(i);
QThread::msleep(100);
}
}
signals:
void progress(int value);
};
这种方式的问题在于:你只能重写run(),而且信号槽的连接方式容易搞错。我个人现在基本不用这种方式了。
方式二:使用工作对象(推荐)
class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork() {
for (int i = 0; i < 100; ++i) {
emit progress(i);
QThread::msleep(100);
}
}
signals:
void progress(int value);
};
// 使用方式
QThread* thread = new QThread;
Worker* worker = new Worker;
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Worker::doWork);
thread->start();
这种方式灵活多了。你可以把任意QObject子类移动到子线程,信号槽也能正常工作。说白了,QThread只是线程的管理器,真正的任务逻辑应该放在QObject里。
3.2 线程的生命周期
嗯,这里有个坑。很多人忘记在程序退出时正确清理线程。我见过一个项目,线程退出时直接terminate(),结果资源没释放,内存泄漏严重。
正确的做法是:
// 请求线程退出
thread->quit();
// 等待线程真正结束(超时3秒)
if (!thread->wait(3000)) {
// 如果超时,强制终止(但尽量别走到这一步)
thread->terminate();
thread->wait();
}
警告: 永远不要在主线程里直接调用terminate()。这会导致线程立即停止,不执行任何清理代码。你想想看,如果线程正在写文件,突然被掐断,文件可能就损坏了。
3.3 信号槽与线程的关系
这是Qt多线程最核心也最容易出错的地方。信号槽的连接方式有5种,但常用的就3种:
| 连接方式 | 说明 | 使用场景 |
|---|---|---|
| AutoConnection(默认) | 发送者和接收者在同一线程,直接调用;在不同线程,排队调用 | 大多数情况用这个就行 |
| DirectConnection | 直接调用,不管在哪个线程 | 需要同步操作时 |
| QueuedConnection | 通过事件循环排队调用 | 跨线程通信时,保证线程安全 |
我刚开始用Qt时,犯过一个经典错误:在子线程里直接更新UI。比如:
// 错误示例:子线程直接操作UI
void Worker::doWork() {
ui->label->setText("处理中..."); // 崩溃!
}
为什么崩溃?因为UI控件不是线程安全的。正确的做法是通过信号槽:
// 正确做法:发射信号,让主线程更新UI
connect(worker, &Worker::updateText, this, [](const QString& text) {
ui->label->setText(text);
});
这里的关键是:信号槽默认是AutoConnection,跨线程时会自动变成QueuedConnection,所以是线程安全的。
四、本章知识体系总览
说了这么多,我画了一张图帮你理清思路:
这张图把本章的核心内容串起来了。从左到右是知识递进,从上到下是层次展开。你把这个图记在脑子里,后面几章学起来会轻松很多。
本章重点回顾:
- 进程是资源容器,线程是执行单元
- 多线程主要用于:界面不卡顿、并发计算、I/O操作
- QThread推荐使用「工作对象+moveToThread」方式
- 跨线程通信必须用信号槽,不要直接操作UI
- 线程退出要优雅:先quit(),再wait()
好了,这一章就到这里。记住我上面说的这些,尤其是那个图,后面讲线程同步、线程池、并发容器的时候,都会用到这些基础概念。