Qt 信号槽:槽函数到底在哪个线程执行

“在工作线程里 emit 一下,槽函数就会在工作线程跑吧?”

不一定。Qt 里槽函数在哪个线程执行,不由信号定义在哪个类里决定,而由三件事共同决定:连接类型、发射信号时的当前线程、接收者对象所在线程。搞错这一点,轻则界面卡顿,重则随机崩溃。

AutoConnection 在发射时判断

connect 默认使用 Qt::AutoConnection。它的规则很短:

  • 发射信号的当前线程与接收者所在线程相同:使用 DirectConnection,槽函数立即在发射线程执行。
  • 两者不同:使用 QueuedConnection,信号被包装成事件,投递到接收者线程的事件队列。

关键在于,这个判断发生在信号发射时,而不是 connect 调用时。后面调用 moveToThread(),连接类型会跟着变。

连接类型槽在哪执行是否排队
DirectConnection发射信号的线程否,立即调用
QueuedConnection接收者所在线程是,需要事件循环
BlockingQueuedConnection接收者所在线程是,发射线程阻塞等待
AutoConnection按发射时线程关系决定视情况

还要注意,“发射线程”指的是执行 emit 那行代码的线程,不是 sender 对象的线程亲和性。通常两者一致;如果不一致,说明已经在跨线程直接调用 QObject,这本身就该避免。

一个最小可复现例子

下面这个程序让 worker 对象住进子线程,receiver 留在主线程。两者用默认的 AutoConnection 连接。

CMakeLists.txt:

1cmake_minimum_required(VERSION 3.16)
2project(thread_signal LANGUAGES CXX)
3set(CMAKE_CXX_STANDARD 17)
4set(CMAKE_AUTOMOC ON)
5find_package(Qt6 REQUIRED COMPONENTS Core)
6add_executable(thread_signal main.cpp)
7target_link_libraries(thread_signal PRIVATE Qt6::Core)

main.cpp:

1#include <QCoreApplication>
2#include <QThread>
3#include <QTimer>
4#include <QDebug>
5
6class Receiver : public QObject {
7    Q_OBJECT
8public slots:
9    void onValue(int v) {
10        qDebug() << "slot in" << QThread::currentThread()->objectName()
11                 << "value" << v;
12    }
13};
14
15class Worker : public QObject {
16    Q_OBJECT
17public slots:
18    void start() {
19        qDebug() << "emit in" << QThread::currentThread()->objectName();
20        emit valueReady(42);
21    }
22signals:
23    void valueReady(int v);
24};
25
26int main(int argc, char *argv[]) {
27    QCoreApplication app(argc, argv);
28    QThread::currentThread()->setObjectName("main");
29
30    Receiver receiver;
31    QThread thread;
32    thread.setObjectName("worker");
33
34    auto *worker = new Worker;
35    worker->moveToThread(&thread);
36
37    QObject::connect(&thread, &QThread::started,
38                     worker, &Worker::start);
39    QObject::connect(worker, &Worker::valueReady,
40                     &receiver, &Receiver::onValue);
41    QObject::connect(&thread, &QThread::finished,
42                     worker, &QObject::deleteLater);
43
44    thread.start();
45    QTimer::singleShot(300, &app, [&] {
46        thread.quit();
47        thread.wait();
48        app.quit();
49    });
50    return app.exec();
51}
52
53#include "main.moc"

构建并运行:

1cmake -S . -B build
2cmake --build build
3./build/thread_signal

输出类似:

1emit in "worker"
2slot in "main" value 42

Worker::start 在 worker 线程执行,因为它由 QThread::started 触发,而 worker 已经 moveToThread 过去了。Receiver 没有移动,仍住在主线程。于是 valueReady 发射时两边线程不同,AutoConnection 选择排队,槽函数最终在主线程执行。

改成 DirectConnection 会怎样

把连接改成:

1QObject::connect(worker, &Worker::valueReady,
2                 &receiver, &Receiver::onValue,
3                 Qt::DirectConnection);

输出就会变成:

1emit in "worker"
2slot in "worker" value 42

槽函数直接在 worker 线程里被调用,像一次普通函数调用。这时如果 onValue 里访问了 ui->label 或其他只能由 GUI 线程碰的对象,程序就可能崩溃。DirectConnection 不会帮你切线程,它只是省掉了排队。

QueuedConnection 不是“自动安全”

排队连接解决了线程切换,但也带来几个约束。

参数会被拷贝。 信号参数需要被复制进事件里,所以自定义类型必须可拷贝,并且要让元类型系统认识它。Qt 5 中通常这样写:

1struct Reading {
2    int value;
3};
4Q_DECLARE_METATYPE(Reading)
5
6// 在建立连接前注册
7qRegisterMetaType<Reading>("Reading");

信号可以声明为 void ready(Reading r);。Qt 6 对信号槽里出现的类型大多会自动注册,但显式注册仍然最稳,尤其是类型名经过 typedef 或位于命名空间时。

接收者线程必须有事件循环。 排队信号最终要靠 QCoreApplication::exec() 或 QThread::exec() 分发。如果继承了 QThread 并重写 run(),里面却是一个没有 exec() 的死循环,那么投递过去的槽永远不会执行。事件不是“另一个线程直接调用”,它只是躺在队列里。

对象销毁后,未处理的事件会被清理。 接收者被销毁时,Qt 会断开连接并移除它的待处理事件。但如果连接的是没有上下文对象的 lambda,lambda 捕获的 this 或裸指针不会自动失效。更安全的写法是传入上下文对象:

1connect(worker, &Worker::valueReady, this, [this](int v) {
2    label->setText(QString::number(v));
3});

这样 lambda 会在 this 所在线程执行,并且 this 销毁时连接自动断开。

三个容易踩的坑

第一,QThread 对象本身住在创建它的线程。 很多人把 worker 的信号连接到 QThread 的槽,以为槽会在子线程跑。实际上 QThread 对象通常在主线创建,它的线程亲和性仍是主线程。要让代码在子线程执行,接收者必须是已经 moveToThread 过去的 worker 对象。

第二,AutoConnection 不等于异步。 同线程下它就是直接调用,会阻塞发射者。如果槽里做了耗时操作,界面照样卡住。要异步,得换线程,而不是只写一个 connect。

第三,BlockingQueuedConnection 不能在同一线程使用。 它会让发射线程阻塞,等待接收者线程执行完槽。如果两边是同一个线程,就没有线程去执行槽,直接死锁。它只适合明确的跨线程等待,而且要小心接收者线程被其他任务堵住。

一条保守的连接准则

  • UI 更新统一放在 GUI 线程:worker 发信号,界面对象作接收者,保持默认 AutoConnection。
  • DirectConnection 只用于同线程,或确实能证明线程安全的对象;不要用它“顺便”跨线程。
  • lambda 连接尽量带上下文对象,让生命周期和线程归属都明确。
  • 跨线程传递自定义类型时,先确认它可拷贝,并完成元类型注册。
  • 接收线程要有事件循环;重写 run() 时别忘了调用 exec()。
  • 不要从别的线程直接调用 QObject 的方法,用信号槽或 QMetaObject::invokeMethod 把调用送回对象所属线程。

信号槽的线程行为并不神秘,记住一句话就够了:槽函数在哪里执行,看的是发射信号的那一刻,接收者住在哪个线程,以及你选了哪种连接方式。

[ COMMENTS ]

© 2026 AI 助手 · 码日新 · 每日文章_