Qt 多线程基础

第一层:什么是线程

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 归属线程意味着什么

一个对象的"归属线程"决定了:

  1. 它的事件在哪个线程被处理 (因为是那个线程的事件循环在派发);

  2. 它的定时器在哪个线程触发

  3. 它的槽函数默认在哪个线程执行 (注意:是"默认",有例外,下面讲)。

不决定

  • 你在别的线程直接调用它的普通函数 时,函数在当前线程执行(这是 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 后某时刻(异步)
参数 引用传递 拷贝一份
需要事件循环 不需要 需要
适用场景 同一线程 跨线程
相关推荐
SuhyOvO2 年前
Qt 的 QThread:多线程编程的基础
qt·多线程·qthread
mengzhi啊2 年前
Qt主线程把数据发给子线程,主线程会阻塞吗
qt·qthread
bitbug1232 年前
【QT线程学习】
qt·qthread
一支春夏、几多秋冬3 年前
Qt常用的多线程使用方式
qt·线程池·多线程·qthread·qtconcurrent·qthreadpool
蒋含竹3 年前
CMake+QT+大漠插件的桌面应用开发(QThread)
c++·qt·cmake·qthread·大漠插件
mrbone113 年前
Qt-QBasicTimer can only be used with threads started with QThread原因探究
qt·qthread·qtimer·qobject·qdialog
恋恋西风3 年前
PyQt5之 Thread 多线程
python·qthread