第一层:什么是线程
1.1 进程 vs 线程
打个比方:
-
进程 :一家公司。有独立的办公场地(内存空间)、独立的营业执照。
-
线程 :公司里的员工。共享 同一个办公场地(内存),但各自干各自的活。
一个程序(进程)启动后,至少有一个线程,叫主线程。
cpp
int main(int argc, char **argv)
{
QApplication app(argc, argv);
// ↑ 从这行开始,你就在【主线程】里
return app.exec();
}
1.2 为什么需要多个线程
因为有些活很慢,如果都在主线程干,界面就会"卡死":
-
读大文件
-
等网络响应
-
串口收数据
-
跑复杂计算
所以开一个子线程去干这些慢活,主线程就继续响应界面。
核心矛盾 :多个线程共享内存 (同一个进程),所以可以互相访问对方的数据------但同时访问同一个数据就会出问题(竞争、崩溃)。
第二层:什么是事件循环
2.1 GUI 程序的工作方式
GUI 程序不是"从上往下跑完就退出",而是一直等在那里,等用户操作:
cpp
程序启动
↓
进入一个死循环:
等事件(鼠标点、键盘按、定时器到、网络来数据......)
处理事件
回到"等事件"
↓
用户关闭窗口 → 退出循环 → 程序结束
这个死循环叫 事件循环(event loop) ,Qt 里就是 app.exec()。
2.2 exec() 到底在干嘛
cpp
int QCoreApplication::exec()
{
while (!quitRequested) {
QEvent *e = eventQueue.take(); // 阻塞等待,直到有事件
dispatch(e); // 把事件分发给对应对象处理
}
}
重点 :事件循环就是"从队列取事件 → 处理 → 再取"的循环。没有它,程序就成了一条直线,处理完就退出了。
2.3 事件是什么
事件就是一个"通知":告诉某个对象发生了什么。
-
鼠标事件 → 发给鼠标下面的 widget
-
定时器事件 → 发给对应QTimer
-
绘制事件 → 发给需要重绘的 widget
-
自定义事件 → 你自己发的
事件都是排队 的,一个一个处理,同一线程内不会同时处理两个事件。这是重要保证。
第三层:Qt 对象与"线程归属"
3.1 QObject 属于哪个线程
每个 QObject 有一个"归属线程"属性 thread():
cpp
// 在主线程里 new 出来的对象
QObject *o = new QObject;
qDebug() << o->thread(); // 主线程
// 在子线程里 new 出来的对象
// (子线程 run() 里 new)
QObject *p = new QObject;
qDebug() << p->thread(); // 那个子线程
规则:在哪个线程中new 的,就属于谁所在的线程。
3.2 归属线程意味着什么
一个对象的"归属线程"决定了:
-
它的事件在哪个线程被处理 (因为是那个线程的事件循环在派发);
-
它的定时器在哪个线程触发;
-
它的槽函数默认在哪个线程执行 (注意:是"默认",有例外,下面讲)。
不决定:
- 你在别的线程直接调用它的普通函数 时,函数在当前线程执行(这是 C++ 规则,Qt 管不着)。
这就是为什么"跨线程直接调对象的方法"是危险的------你绕过了 Qt 的线程保护机制。
3.3 举例
cpp
// 主线程:
QSerialPort *port = new QSerialPort; // port 归属主线程
port->open();
// 子线程里的代码:
port->write("hello"); // ← 危险!在子线程里调用了主线程对象的方法
port->write() 里访问的底层资源(句柄、缓冲区)都跟主线程的事件循环绑定。子线程跑过去写,就像两个员工同时改同一份文件------出问题是迟早的。
第四层:信号与槽
4.1 是什么
Qt 的核心机制,用来解耦对象之间的通信:
cpp
connect(发送者, &发送者类::信号, 接收者, &接收者类::槽);
-
信号:某个对象身上发生的事,"我发了个信号告诉别人"
-
槽:一个普通函数,被连接到信号后,信号触发时会调用它
cpp
class Button : public QObject {
Q_OBJECT
signals:
void clicked(); // 信号:只声明,不实现
};
class Window : public QObject {
Q_OBJECT
public slots:
void onButtonClicked() { // 槽:普通函数
qDebug() << "按钮被点了";
}
};
// 连接
Button *btn = new Button;
Window *win = new Window;
connect(btn, &Button::clicked, win, &Window::onButtonClicked);
// 触发
emit btn->clicked(); // → onButtonClicked() 会被调用
4.2 最常见的困惑:emit 后槽什么时候执行
答案:取决于连接方式。 这正是所有多线程问题的核心。
4.3 同一线程的情况(先看简单的)
当发送者和接收者属于同一线程:
cpp
// 都在主线程
connect(btn, &Button::clicked, win, &Window::onButtonClicked);
emit btn->clicked();
// ↓ 立刻、马上、同步执行:
// onButtonClicked() 在这里执行
// 执行完才继续往下
qDebug() << "emit 之后";
执行顺序:
cpp
emit clicked();
→ onButtonClicked() 执行
qDebug() << "emit 之后";
这就是直连(DirectConnection),你平时的代码基本都这样。看起来"信号就是函数调用",其实是错觉。
第五层:跨线程时怎么办
5.1 问题来了
如果发送者在子线程,接收者在主线程:
cpp
connect(worker, &Worker::dataReady, // worker 在子线程
win, &MainWindow::onData); // win 在主线程
// 子线程里:
emit worker->dataReady(data);
如果直接同步调用 onData:
-
onData就会在子线程执行; -
但
onData里可能碰 UI(主线程的资源); -
子线程碰主线程的东西 → 崩。
所以 Qt 需要一个机制 :让 onData 在它该在的线程(主线程)执行,而不是在 emit 的线程执行。
这个机制就是 QueuedConnection(队列连接)。
5.2 Qt 怎么做的
思路很简单:
既然不能直接调,那就把"要调谁、带什么参数"打包成一封信 ,寄到接收者线程的邮箱(事件队列) 里。接收者线程的事件循环去取信,在自己的线程里读信、执行函数。
具体:
cpp
// emit worker->dataReady(data) 实际执行:
1. 创建一个 QMetaCallEvent:
{
slot = &MainWindow::onData, // 要调什么
arg = data(拷贝一份) // 参数
target = win // 谁的事
}
2. postEvent(win, event);
// ↑ 把事件塞进 win 所在线程(主线程)的事件队列
3. emit 返回,子线程继续跑
主线程的事件循环稍后取到这个事件,执行:
cpp
win->onData(data); // ← 在【主线程】执行
5.3 对比 DirectConnection
| DirectConnection | QueuedConnection | |
|---|---|---|
| 怎么执行 | 直接调用 | 打包成事件投递 |
| 槽在哪个线程 | emit 的线程 | 接收者所属线程 |
| 时机 | emit 时立即执行 | emit 后某时刻(异步) |
| 参数 | 引用传递 | 拷贝一份 |
| 需要事件循环 | 不需要 | 需要 |
| 适用场景 | 同一线程 | 跨线程 |