一、前言
在学qt槽函数的时候,一直想不清楚内部槽函数怎么调用的,是否有多线程并发执行,还是单线程单独执行。
本来设计了一个实验来探索qt槽函数中的线程机制。
二、实验

两个按钮,压入按钮负责触发两个槽函数slot1、slot2,分别往一个多线程同步队列压入奇数和偶数,弹出按钮触发槽函数slot,将同步队列中的元素依次弹出。
如果队列输出结果是奇数偶数交替的,那么槽函数就是由多线程来执行的,反之如果不是交替的,就是单线程。
代码如下:
MainWindow.h
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "syncqueue.h"
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
// SyncQueue<int> sq(1000);
SyncQueue<int> sq{20000005};
private slots:
void slot1();
void slot2();
void slot();
};
#endif // MAINWINDOW_H
MainWindow.cpp
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(ui->pushButton,&QPushButton::clicked,this,&MainWindow::slot1);
connect(ui->pushButton,&QPushButton::clicked,this,&MainWindow::slot2);
connect(ui->pushButton_2,&QPushButton::clicked,this,&MainWindow::slot);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::slot1()
{
// 循环输出若干个奇数
for(int i = 1; i < 200000; i += 2){
qDebug() << i;
sq.push(i);
}
}
void MainWindow::slot2()
{
// 循环输出若干个偶数
for(int i = 0; i < 200000; i += 2){
qDebug() << i;
sq.push(i);
}
}
void MainWindow::slot()
{
sq.clear();
}
syncqueue.h
cpp
#ifndef SYNCq_H
#define SYNCq_H
#include <mutex>
//#include <iostream>
//#include <optional>
#include <atomic>
#include <QQueue>
#include <queue>
#include <condition_variable>
#include <QDebug>
//using namespace std;
//实现一个多线程安全的同步队列, C++
template <typename T>
struct SyncQueue {
public:
SyncQueue(size_t capacity) : capacity(capacity), abort_flag(false) {}
SyncQueue() = delete;
SyncQueue(const SyncQueue<T>&) = delete;
SyncQueue& operator=(const SyncQueue<T>&) = delete;
~SyncQueue() {}
// 唤醒被阻塞的线程并退出
void abort() {
// abort_flag;
q_not_full.notify_all();
q_not_empty.notify_all();
abort_flag = true;
}
// 尝试向队列添加元素,如果失败立即返回
bool try_push(const T& event) {
std::lock_guard<std::mutex> lock(q_mutex);
if(q.size() == capacity){
return false;
}
q.enqueue(event);
return true;
}
// 尝试从队列取出一个元素,如果失败立即返回
// std::optional<T> try_pop() {
void try_pop(){
std::lock_guard<std::mutex> lock(q_mutex);
if(q.empty()){
return;
}
q.dequeue();
//???
}
// 向队列尾部添加元素,如果队列满则等待
bool push(const T& event) {
std::unique_lock<std::mutex> lock(q_mutex); // unique_lock可以解锁
// std::lock_guard<std::mutex> lock(q_mutex); //会发生报错,qt编译不通过,因为lock_guard只能锁定一次,不能解锁,知道析构,
q_not_full.wait(lock,[&](){
if(abort_flag){
return false;//?
}
if(q.size() == capacity){
return false;// 待定
}
return true;
});
q.enqueue(event);
q_not_full.notify_one();
return true;
}
// 从队列取出并删除头部元素,如果队列空则等待
// std::optional<T> pop() {
void pop(){
std::unique_lock<std::mutex> lock(q_mutex);
q_not_empty.wait(lock,[&](){
if(abort_flag){
return false;//?
}
if(q.size()){
return false;// 待定
}
return true;
});
q_not_empty.notify_one();
q.dequeue();
}
size_t length() {
std::lock_guard<std::mutex> lock(q_mutex);
return q.size();
}
bool full() {
std::lock_guard<std::mutex> lock(q_mutex);
// if(q.size() == capacity){
// return true;
// }
// return false;
return q.size() == capacity;
}
bool empty() {
std::lock_guard<std::mutex> lock(q_mutex);
return q.empty();
}
void clear() {
std::lock_guard<std::mutex> lock(q_mutex);
while(!q.empty()){
qDebug() << q.head();
q.dequeue();
}
}
private:
size_t capacity;
std::atomic<bool> abort_flag;
// std::queue<T> q; //在这个qt版本里面需要使用std::queue<T,T>才可以成功编译,太怪啦
QQueue<T> q;
std::mutex q_mutex;
std::condition_variable q_not_full;
std::condition_variable q_not_empty;
};
#endif // SYNCq_H
最后的实验结果是 先压入所有的单数,再压入所有的双数。
三、qt槽函数的线程机制
qt槽函数的线程机制其实和其ui的线程机制有关。qt的ui相关操作都是在主线程的,而且整个qt程序默认只有一个主线程,除非自己新开线程。在多线程情况下,槽函数执行的线程选择一般是根据信号的发出者或接收者来选择执行线程的。
再下面的,我写不动啦而且也实在不会啦,看看ai解释吧。
3.1 qt中同一个信号触发多个槽函数,会有先后顺序,解释背后的原因
在 Qt 中,当一个信号被触发时,与其相连的所有槽函数会按照建立连接的顺序依次执行。这是 Qt 框架明确保证的行为规范,背后的原因与其内部实现机制密切相关。
核心原因:Qt 使用有序的连接列表
-
数据结构 :每个
QObject实例内部维护着一个连接列表(Connection List) ,用于存储所有与该对象信号相连的槽函数信息。这个列表是一个有序的线性结构,每当一个新的连接被建立(connect()),对应的连接信息(如接收者、槽函数指针等)就会被追加到这个列表的末尾。 -
信号触发时的遍历机制 :当信号被
emit触发时,Qt 的元对象系统(Meta-Object System)会查找并按顺序遍历这个连接列表。对于列表中的每一个有效连接,Qt 会依次调用其对应的槽函数。遍历完成后,信号发射过程结束。
不同连接类型的影响
连接类型(Qt::ConnectionType)决定了槽函数执行的时间和方式,但不会改变它们在连接列表中的相对顺序:
- 直连(
Qt::DirectConnection) :信号发射是立即 、同步 的。槽函数在信号发射的线程中被直接调用,就像调用普通函数一样。所有槽函数会按顺序一口气执行完,信号发射语句之后的代码要等到所有槽函数执行完毕后才会继续。 - 队列连接(
Qt::QueuedConnection) :信号发射是异步 的。Qt 会为每个槽函数创建一个事件,并按顺序 投递到接收者线程的事件队列中。这些事件会在线程处理事件循环时按顺序被取出并执行对应的槽函数。因此,宏观上看槽函数的执行顺序依然遵循连接顺序,但它们是分散在事件循环中执行的。 - 自动连接(
Qt::AutoConnection) :这是默认类型。Qt 会根据信号发射线程和接收者线程是否相同,自动选择DirectConnection或QueuedConnection。无论哪种,连接顺序依然是执行的基准。 - 混合连接 :如果一个信号同时连接了直连和队列槽函数,Qt 仍然按连接列表顺序处理。直连槽会立即同步执行,队列槽会按序压入事件队列。这可能导致"即时"与"延迟"执行的交错,但发起执行的顺序不变。
注意事项
虽然顺序有保证,但从软件设计角度看,强烈建议不要编写逻辑上依赖槽函数执行顺序的代码 。如果多个槽之间有严格的先后依赖,它们应该由同一个槽函数来协调,或槽函数之间不应有副作用耦合。依赖 connect() 的书写顺序会使代码脆弱且难以维护。
总结 :顺序性的根本原因是 Qt 内部使用了一个按连接操作顺序追加的有序列表,并在信号触发时进行线性遍历。这是其信号槽机制实现的基础,而非巧合。
3.2 这个和事件队列有什么联系没
有直接联系,但仅限于队列连接(QueuedConnection)场景 。信号槽的顺序保证 与事件队列的FIFO特性共同维护了执行顺序,但两者本质上是 Qt 中独立的通信机制。
不同连接类型的处理方式
| 连接类型 | 是否涉及事件队列 | 执行方式 | 顺序保证来源 |
|---|---|---|---|
Qt::DirectConnection |
❌ 完全无关 | 直接函数调用,同步执行 | 连接列表的顺序遍历 |
Qt::QueuedConnection |
✅ 深度依赖 | 将槽调用封装为事件,异步投递 | 连接列表顺序 + 事件队列FIFO |
Qt::BlockingQueuedConnection |
✅ 深度依赖 | 投递事件并阻塞等待完成 | 同上 |
Qt::AutoConnection |
视情况而定 | 同线程=直连,跨线程=队列 | 同上 |
队列连接与事件队列的协作机制
当信号跨线程或使用 QueuedConnection 时:
-
封装为事件 :每个槽函数的调用信息(接收者、槽索引、参数)被封装成一个
QMetaCallEvent对象(Qt 内部事件类型)。 -
按序入队 :这些事件按照连接列表的顺序 ,依次通过
postEvent()投递到接收者线程 的事件队列(QEventQueue)尾部。此时信号发射结束,继续执行后续代码。 -
事件循环处理 :接收者线程在事件循环(
QCoreApplication::exec())中不断取出事件。由于是 FIFO 队列,事件会按投递顺序被依次处理。 -
执行槽函数 :当
QMetaCallEvent被处理时,Qt 从中解析出槽函数信息并执行。因此,槽函数最终按连接顺序被调用。
cpp
// 伪代码示意:队列连接的底层逻辑
void signalEmitter() {
for (Connection& conn : connectionsList) { // 按连接顺序遍历
if (conn.type == QueuedConnection) {
// 创建事件并投递到接收者线程队列
postEvent(conn.receiver, new QMetaCallEvent(...));
}
}
}
混合连接的复杂场景
当一个信号同时连接了直连槽 和队列槽:
cpp
connect(obj, &MyClass::mySignal, receiver1, &Receiver::slotDirect, Qt::DirectConnection);
connect(obj, &MyClass::mySignal, receiver2, &Receiver::slotQueued); // 默认跨线程=队列
- 执行顺序 :
slotDirect立即 同步执行 →slotQueued被延迟入队(若在同一线程,可能在下一次事件循环处理)。 - 关键 :入队顺序 依然遵循连接列表顺序,但直连槽的"插队"行为可能导致宏观上的时间错序。不过从机制上,发起调用的顺序始终不变。
总结
- 直连场景 :信号槽与事件队列完全解耦,是纯函数调用。
- 队列场景 :信号槽利用 事件队列实现跨线程异步通信 ,此时事件队列的 FIFO 特性是槽函数最终有序执行的保障。
- 本质区别 :信号槽列表是静态的连接顺序 ,事件队列是动态的调度顺序。两者共同协作,使跨线程信号发射既能保证顺序,又能实现非阻塞异步调用。