基于Qt的MetaCall事件实现的线程安全事件发布订阅系统,类似ROS的消息订阅模式。
特性
- ✅ 线程安全:使用互斥锁保护订阅者列表,支持多线程环境
- ✅ 多种发布模式:支持同步、异步当前线程、异步接收者三种发布模式
- ✅ 灵活的回调:支持lambda表达式和函数对象作为回调
- ✅ 自动管理:基于Qt的事件循环,自动处理线程间通信
- ✅ 类型安全:使用QVariant传递数据,支持任意类型
- ✅ 单例模式:全局唯一实例,方便使用
架构设计
代码
eventpubsub.h
cpp
#ifndef EVENTPUBSUB_H
#define EVENTPUBSUB_H
#include <QObject>
#include <QString>
#include <QMap>
#include <QList>
#include <QMutex>
#include <QVariant>
#include <QThread>
#include <QEvent>
#include <functional>
#include <memory>
/**
* @brief 发布类型枚举
*/
enum class PublishType {
Sync, // 同步:在当前线程立即执行
AsyncCurrent, // 异步当前线程:在当前线程的事件循环中执行
AsyncReceiver // 异步接收者:在接收者对象所在线程的事件循环中执行
};
/**
* @brief 订阅者信息结构体
*/
struct SubscriberInfo {
QObject *receiver; // 订阅者对象指针
std::function<void(const QVariant &)> callback; // 回调函数(lambda或函数对象)
QThread *receiverThread; // 接收者所在线程(用于异步接收者模式)
SubscriberInfo() : receiver(nullptr), receiverThread(nullptr) {}
SubscriberInfo(QObject *recv, std::function<void(const QVariant &)> cb)
: receiver(recv), callback(cb), receiverThread(recv ? recv->thread() : nullptr) {}
// 用于比较和查找
bool operator==(const SubscriberInfo &other) const {
return receiver == other.receiver &&
receiverThread == other.receiverThread;
}
};
/**
* @brief MetaCall事件类,用于在事件循环中执行回调
*/
class CallbackEvent : public QEvent {
public:
static QEvent::Type EventType;
CallbackEvent(std::function<void(const QVariant &)> cb, const QVariant &data)
: QEvent(EventType), m_callback(cb), m_data(data) {}
void execute() {
if (m_callback) {
m_callback(m_data);
}
}
private:
std::function<void(const QVariant &)> m_callback;
QVariant m_data;
};
/**
* @brief 接收者辅助对象,用于在接收者线程中处理回调事件
*/
class ReceiverHelper : public QObject {
Q_OBJECT
public:
explicit ReceiverHelper(QObject *parent = nullptr) : QObject(parent) {}
protected:
bool event(QEvent *e) override {
if (e->type() == CallbackEvent::EventType) {
CallbackEvent *callbackEvent = static_cast<CallbackEvent *>(e);
callbackEvent->execute();
return true;
}
return QObject::event(e);
}
};
/**
* @brief 事件发布订阅系统
*
* 基于Qt的MetaCall事件实现的线程安全事件发布订阅系统
* 支持同步和异步两种发布模式
*/
class EventPubSub : public QObject {
Q_OBJECT
public:
/**
* @brief 获取单例实例
*/
static EventPubSub* instance();
/**
* @brief 订阅事件
* @param topicId 主题ID(消息ID)
* @param receiver 订阅者对象指针
* @param callback 回调函数(lambda或函数对象)
* @return 是否订阅成功
*/
bool subscribe(const QString &topicId,
QObject *receiver,
std::function<void(const QVariant &)> callback);
/**
* @brief 取消订阅
* @param topicId 主题ID
* @param receiver 订阅者对象指针
* @return 是否取消成功
*/
bool unsubscribe(const QString &topicId, QObject *receiver);
/**
* @brief 取消某个对象的所有订阅
* @param receiver 订阅者对象指针
* @return 取消的订阅数量
*/
int unsubscribeAll(QObject *receiver);
/**
* @brief 发布消息
* @param topicId 主题ID
* @param data 消息数据(QVariant类型,可以包含任意类型)
* @param type 发布类型(默认异步接收者)
*/
void publish(const QString &topicId,
const QVariant &data,
PublishType type = PublishType::AsyncReceiver);
/**
* @brief 获取某个主题的订阅者数量
* @param topicId 主题ID
* @return 订阅者数量
*/
int getSubscriberCount(const QString &topicId) const;
/**
* @brief 获取所有主题ID列表
* @return 主题ID列表
*/
QStringList getAllTopics() const;
/**
* @brief 清空所有订阅
*/
void clearAll();
protected:
/**
* @brief 自定义事件处理
*/
bool event(QEvent *e) override;
private:
explicit EventPubSub(QObject *parent = nullptr);
~EventPubSub();
/**
* @brief 同步执行回调
*/
void executeCallbackSync(const SubscriberInfo &subscriber, const QVariant &data);
/**
* @brief 异步执行回调(当前线程)
*/
void executeCallbackAsyncCurrent(const SubscriberInfo &subscriber, const QVariant &data);
/**
* @brief 异步执行回调(接收者线程)
*/
void executeCallbackAsyncReceiver(const SubscriberInfo &subscriber, const QVariant &data);
/**
* @brief 获取或创建接收者的辅助对象(用于处理回调事件)
*/
QObject* getOrCreateReceiverHelper(QObject *receiver);
// 订阅者映射表:Map<主题ID, 订阅者列表>
QMap<QString, QList<SubscriberInfo>> m_subscribers;
// 接收者辅助对象映射:Map<接收者对象, 辅助对象>
QMap<QObject*, ReceiverHelper*> m_receiverHelpers;
// 线程安全锁
mutable QMutex m_mutex;
// 单例实例
static EventPubSub* s_instance;
static QMutex s_instanceMutex;
};
#endif // EVENTPUBSUB_H
eventpubsub.cpp
cpp
#include "eventpubsub.h"
#include <QCoreApplication>
#include <QThread>
#include <QDebug>
// 静态成员初始化
EventPubSub* EventPubSub::s_instance = nullptr;
QMutex EventPubSub::s_instanceMutex;
// 自定义事件类型
QEvent::Type CallbackEvent::EventType = static_cast<QEvent::Type>(QEvent::User + 1);
EventPubSub::EventPubSub(QObject *parent)
: QObject(parent)
{
}
EventPubSub::~EventPubSub()
{
clearAll();
}
EventPubSub* EventPubSub::instance()
{
if (!s_instance) {
QMutexLocker locker(&s_instanceMutex);
if (!s_instance) {
s_instance = new EventPubSub();
}
}
return s_instance;
}
bool EventPubSub::subscribe(const QString &topicId,
QObject *receiver,
std::function<void(const QVariant &)> callback)
{
if (!receiver || !callback) {
qWarning() << "EventPubSub::subscribe: Invalid receiver or callback";
return false;
}
QMutexLocker locker(&m_mutex);
SubscriberInfo info(receiver, callback);
// 检查是否已经订阅
if (m_subscribers.contains(topicId)) {
QList<SubscriberInfo> &subscribers = m_subscribers[topicId];
for (const SubscriberInfo &existing : subscribers) {
if (existing == info) {
qWarning() << "EventPubSub::subscribe: Already subscribed to topic:" << topicId;
return false;
}
}
}
// 添加到订阅列表
m_subscribers[topicId].append(info);
qDebug() << "EventPubSub: Subscribed to topic" << topicId
<< "with receiver" << receiver;
return true;
}
bool EventPubSub::unsubscribe(const QString &topicId, QObject *receiver)
{
if (!receiver) {
qWarning() << "EventPubSub::unsubscribe: Invalid receiver";
return false;
}
QMutexLocker locker(&m_mutex);
if (!m_subscribers.contains(topicId)) {
qWarning() << "EventPubSub::unsubscribe: Topic not found:" << topicId;
return false;
}
QList<SubscriberInfo> &subscribers = m_subscribers[topicId];
int removedCount = 0;
// 移除所有匹配的订阅者
for (int i = subscribers.size() - 1; i >= 0; --i) {
if (subscribers[i].receiver == receiver) {
subscribers.removeAt(i);
removedCount++;
}
}
// 如果列表为空,移除主题
if (subscribers.isEmpty()) {
m_subscribers.remove(topicId);
}
// 检查该receiver是否还有其他订阅
bool hasOtherSubscriptions = false;
if (removedCount > 0) {
for (const QList<SubscriberInfo> &subList : m_subscribers.values()) {
for (const SubscriberInfo &info : subList) {
if (info.receiver == receiver) {
hasOtherSubscriptions = true;
break;
}
}
if (hasOtherSubscriptions) {
break;
}
}
// 如果没有其他订阅,清理辅助对象
if (!hasOtherSubscriptions && m_receiverHelpers.contains(receiver)) {
ReceiverHelper *helper = m_receiverHelpers[receiver];
m_receiverHelpers.remove(receiver);
helper->deleteLater();
}
qDebug() << "EventPubSub: Unsubscribed from topic" << topicId
<< "for receiver" << receiver;
}
return removedCount > 0;
}
int EventPubSub::unsubscribeAll(QObject *receiver)
{
if (!receiver) {
return 0;
}
QMutexLocker locker(&m_mutex);
int totalRemoved = 0;
QStringList topicsToRemove;
// 遍历所有主题
for (auto it = m_subscribers.begin(); it != m_subscribers.end(); ++it) {
QList<SubscriberInfo> &subscribers = it.value();
int removedCount = 0;
// 移除所有匹配的订阅者
for (int i = subscribers.size() - 1; i >= 0; --i) {
if (subscribers[i].receiver == receiver) {
subscribers.removeAt(i);
removedCount++;
}
}
totalRemoved += removedCount;
// 如果列表为空,标记为删除
if (subscribers.isEmpty()) {
topicsToRemove.append(it.key());
}
}
// 移除空主题
for (const QString &topic : topicsToRemove) {
m_subscribers.remove(topic);
}
// 清理辅助对象(如果没有其他订阅了)
if (totalRemoved > 0 && m_receiverHelpers.contains(receiver)) {
ReceiverHelper *helper = m_receiverHelpers[receiver];
m_receiverHelpers.remove(receiver);
helper->deleteLater();
}
if (totalRemoved > 0) {
qDebug() << "EventPubSub: Unsubscribed all topics for receiver" << receiver
<< "Total:" << totalRemoved;
}
return totalRemoved;
}
void EventPubSub::publish(const QString &topicId,
const QVariant &data,
PublishType type)
{
QMutexLocker locker(&m_mutex);
if (!m_subscribers.contains(topicId)) {
qDebug() << "EventPubSub::publish: No subscribers for topic:" << topicId;
return;
}
// 复制订阅者列表(避免在回调中持有锁)
QList<SubscriberInfo> subscribers = m_subscribers[topicId];
locker.unlock();
// 根据发布类型执行回调
for (const SubscriberInfo &subscriber : subscribers) {
// 检查接收者是否仍然有效
if (!subscriber.receiver) {
continue;
}
switch (type) {
case PublishType::Sync:
executeCallbackSync(subscriber, data);
break;
case PublishType::AsyncCurrent:
executeCallbackAsyncCurrent(subscriber, data);
break;
case PublishType::AsyncReceiver:
executeCallbackAsyncReceiver(subscriber, data);
break;
}
}
}
void EventPubSub::executeCallbackSync(const SubscriberInfo &subscriber, const QVariant &data)
{
// 同步执行:直接在当前线程调用
if (subscriber.callback) {
subscriber.callback(data);
}
}
void EventPubSub::executeCallbackAsyncCurrent(const SubscriberInfo &subscriber, const QVariant &data)
{
// 异步当前线程:通过事件循环执行
if (!subscriber.callback) {
return;
}
// 创建回调事件
CallbackEvent *event = new CallbackEvent(subscriber.callback, data);
// 投递到当前线程的事件队列
QCoreApplication::postEvent(static_cast<QObject*>(this), event);
}
QObject* EventPubSub::getOrCreateReceiverHelper(QObject *receiver)
{
if (!receiver) {
return nullptr;
}
QMutexLocker locker(&m_mutex);
// 检查是否已存在辅助对象
if (m_receiverHelpers.contains(receiver)) {
return m_receiverHelpers[receiver];
}
// 创建新的辅助对象(不设置父对象,避免跨线程问题)
// 先创建对象,然后移动到接收者线程,这样就不会有跨线程父子关系的问题
ReceiverHelper *helper = new ReceiverHelper(nullptr);
helper->moveToThread(receiver->thread());
m_receiverHelpers[receiver] = helper;
return helper;
}
void EventPubSub::executeCallbackAsyncReceiver(const SubscriberInfo &subscriber, const QVariant &data)
{
// 异步接收者:在接收者对象所在线程的事件循环中执行
if (!subscriber.callback || !subscriber.receiver) {
return;
}
// 检查接收者对象是否仍然有效
if (!subscriber.receiver->thread()) {
return;
}
// 获取或创建辅助对象
QObject *helper = getOrCreateReceiverHelper(subscriber.receiver);
if (!helper) {
return;
}
// 创建回调事件
CallbackEvent *event = new CallbackEvent(subscriber.callback, data);
// 投递到辅助对象所在线程的事件队列(与接收者同一线程)
QCoreApplication::postEvent(helper, event);
}
bool EventPubSub::event(QEvent *e)
{
if (e->type() == CallbackEvent::EventType) {
CallbackEvent *callbackEvent = static_cast<CallbackEvent *>(e);
callbackEvent->execute();
return true;
}
// 调用基类的event方法
return QObject::event(e);
}
int EventPubSub::getSubscriberCount(const QString &topicId) const
{
QMutexLocker locker(&m_mutex);
if (m_subscribers.contains(topicId)) {
return m_subscribers[topicId].size();
}
return 0;
}
QStringList EventPubSub::getAllTopics() const
{
QMutexLocker locker(&m_mutex);
return m_subscribers.keys();
}
void EventPubSub::clearAll()
{
QMutexLocker locker(&m_mutex);
// 清理所有辅助对象
for (ReceiverHelper *helper : m_receiverHelpers.values()) {
helper->deleteLater();
}
m_receiverHelpers.clear();
// 清理所有订阅
m_subscribers.clear();
qDebug() << "EventPubSub: Cleared all subscriptions";
}
核心组件
- EventPubSub类:事件发布订阅系统的核心类
- SubscriberInfo结构体:存储订阅者信息(接收者对象、回调函数、线程信息)
- CallbackEvent类:自定义事件类,用于在事件循环中执行回调
- PublishType枚举:定义三种发布类型
数据结构
Map<QString, QList<SubscriberInfo>>
├── Key: 主题ID(消息ID)
└── Value: 订阅者列表
└── SubscriberInfo
├── receiver: QObject指针
├── callback: 回调函数
└── receiverThread: 接收者所在线程
线程安全机制
- 使用
QMutex保护订阅者映射表 - 订阅和取消订阅操作都加锁保护
- 发布消息时复制订阅者列表,避免长时间持有锁
技术详解
与Qt信号槽机制的关系
本组件基于Qt的事件驱动机制实现,与Qt信号槽系统有相似之处,但也有重要区别:
相似点:
- 都基于Qt的事件循环机制
- 都支持异步执行和跨线程通信
- 都使用事件队列实现延迟执行
区别:
- 信号槽:需要预定义信号和槽函数,编译时检查,类型安全
- 发布订阅:动态订阅,运行时绑定,使用QVariant传递数据,更灵活
实现对比:
cpp
// Qt信号槽方式
class Sender : public QObject {
Q_OBJECT
signals:
void dataReady(int value); // 需要预定义信号
};
class Receiver : public QObject {
Q_OBJECT
public slots:
void onDataReady(int value); // 需要预定义槽函数
};
connect(sender, &Sender::dataReady, receiver, &Receiver::onDataReady);
// 发布订阅方式(本组件)
EventPubSub* pubSub = EventPubSub::instance();
pubSub->subscribe("topic", receiver, [](const QVariant &data) {
// 动态回调,无需预定义
int value = data.toInt();
});
关键技术点
1. ReceiverHelper辅助对象的设计
为什么需要辅助对象?
在AsyncReceiver模式下,我们需要将回调事件投递到接收者对象所在线程的事件队列中执行。但是,直接向接收者对象投递自定义事件有一个问题:接收者对象可能没有重写event()方法来处理我们的CallbackEvent。
解决方案:
我们为每个接收者创建一个ReceiverHelper辅助对象,这个对象专门用来处理回调事件:
cpp
class ReceiverHelper : public QObject {
Q_OBJECT
protected:
bool event(QEvent *e) override {
if (e->type() == CallbackEvent::EventType) {
CallbackEvent *callbackEvent = static_cast<CallbackEvent *>(e);
callbackEvent->execute(); // 执行回调
return true;
}
return QObject::event(e);
}
};
工作流程:
发布消息(AsyncReceiver模式)
↓
获取或创建ReceiverHelper(与receiver同一线程)
↓
创建CallbackEvent事件
↓
投递到ReceiverHelper的事件队列
↓
ReceiverHelper的event()方法处理事件
↓
执行回调函数
2. 为什么不能用receiver作为父对象?
这是本组件设计中的一个关键技术点,涉及到Qt对象父子关系和线程安全。
问题分析:
cpp
// ❌ 错误的方式
ReceiverHelper *helper = new ReceiverHelper(receiver); // receiver作为父对象
helper->moveToThread(receiver->thread());
为什么不能这样做?
-
Qt的父子关系规则:
- Qt要求父子对象必须在同一个线程中
- 如果父对象和子对象在不同线程,会导致未定义行为
- 当对象被移动到另一个线程时,其所有子对象也会被移动
-
线程移动的时机问题:
cpp// 假设receiver在主线程,EventPubSub在另一个线程 ReceiverHelper *helper = new ReceiverHelper(receiver); // 此时helper也在EventPubSub的线程中(因为父对象receiver可能还没移动到目标线程) helper->moveToThread(receiver->thread()); // 移动helper时,Qt会检查父子关系,可能导致问题 -
跨线程父子关系的风险:
- 如果receiver和EventPubSub在不同线程,创建父子关系会违反Qt的线程规则
- 可能导致对象在错误的线程中被删除
- 可能导致事件处理在错误的线程中执行
正确的实现方式:
cpp
// ✅ 正确的方式
ReceiverHelper *helper = new ReceiverHelper(nullptr); // 不设置父对象
helper->moveToThread(receiver->thread()); // 先移动到目标线程
m_receiverHelpers[receiver] = helper; // 手动管理生命周期
关键要点:
-
先创建对象,再移动线程:
- 创建时不设置父对象(使用
nullptr) - 然后调用
moveToThread()移动到接收者线程 - 这样避免了跨线程父子关系的问题
- 创建时不设置父对象(使用
-
手动管理生命周期:
- 不使用Qt的父子关系自动删除
- 在
unsubscribe()或clearAll()时手动删除辅助对象 - 使用
deleteLater()确保在正确的线程中删除
-
线程安全考虑:
- 辅助对象的创建和删除都在锁保护下进行
- 确保多线程环境下的安全性
代码实现:
cpp
QObject* EventPubSub::getOrCreateReceiverHelper(QObject *receiver)
{
if (!receiver) {
return nullptr;
}
QMutexLocker locker(&m_mutex);
// 检查是否已存在
if (m_receiverHelpers.contains(receiver)) {
return m_receiverHelpers[receiver];
}
// 创建新的辅助对象(不设置父对象,避免跨线程问题)
// 先创建对象,然后移动到接收者线程,这样就不会有跨线程父子关系的问题
ReceiverHelper *helper = new ReceiverHelper(nullptr);
helper->moveToThread(receiver->thread());
m_receiverHelpers[receiver] = helper;
return helper;
}
3. 基于MetaCall事件的实现原理
本组件使用Qt的QEvent机制实现异步回调,这与Qt信号槽的QMetaCallEvent有相似之处。
事件类型定义:
cpp
// 自定义事件类型
QEvent::Type CallbackEvent::EventType = static_cast<QEvent::Type>(QEvent::User + 1);
事件类设计:
cpp
class CallbackEvent : public QEvent {
public:
CallbackEvent(std::function<void(const QVariant &)> cb, const QVariant &data)
: QEvent(EventType), m_callback(cb), m_data(data) {}
void execute() {
if (m_callback) {
m_callback(m_data); // 执行回调
}
}
private:
std::function<void(const QVariant &)> m_callback;
QVariant m_data;
};
事件处理流程:
1. 发布消息时创建CallbackEvent
↓
2. 通过QCoreApplication::postEvent()投递到事件队列
↓
3. 事件循环从队列取出事件
↓
4. 调用目标对象的event()方法
↓
5. ReceiverHelper的event()方法识别CallbackEvent
↓
6. 调用CallbackEvent::execute()
↓
7. 执行回调函数
与Qt信号槽的MetaCallEvent对比:
| 特性 | Qt信号槽 (QMetaCallEvent) | 本组件 (CallbackEvent) |
|---|---|---|
| 事件类型 | Qt内部定义 | 自定义(QEvent::User + 1) |
| 调用方式 | 通过元对象系统 | 通过std::function |
| 类型检查 | 编译时检查 | 运行时检查(QVariant) |
| 灵活性 | 需要预定义 | 动态回调 |
4. 线程安全的订阅管理
锁的使用策略:
-
订阅操作:全程持有锁
cppbool EventPubSub::subscribe(...) { QMutexLocker locker(&m_mutex); // 自动加锁 // 检查、添加订阅者 // 锁在函数返回时自动释放 } -
发布操作:复制列表后释放锁
cppvoid EventPubSub::publish(...) { QMutexLocker locker(&m_mutex); QList<SubscriberInfo> subscribers = m_subscribers[topicId]; // 复制 locker.unlock(); // 立即释放锁 // 在锁外执行回调,避免长时间持有锁 for (const SubscriberInfo &subscriber : subscribers) { executeCallback(...); } }
为什么发布时要复制列表?
- 避免在回调执行期间长时间持有锁
- 防止死锁(回调函数可能再次调用subscribe/unsubscribe)
- 提高并发性能
5. 内存管理策略
辅助对象的生命周期:
- 创建时机:首次订阅时创建
- 删除时机 :
- 取消订阅时检查是否还有其他订阅
- 如果没有,删除辅助对象
- 使用
deleteLater()确保在正确的线程中删除
cpp
// 取消订阅时的清理逻辑
if (!hasOtherSubscriptions && m_receiverHelpers.contains(receiver)) {
ReceiverHelper *helper = m_receiverHelpers[receiver];
m_receiverHelpers.remove(receiver);
helper->deleteLater(); // 在正确的线程中删除
}
为什么使用deleteLater()?
- Qt要求对象必须在创建它的线程中删除
deleteLater()将删除操作放入事件队列- 确保在正确的线程中执行删除
与Qt信号槽的深度对比
| 维度 | Qt信号槽 | 本组件(发布订阅) |
|---|---|---|
| 定义方式 | 编译时定义(signals/slots) | 运行时动态订阅 |
| 类型安全 | 强类型(编译时检查) | 弱类型(QVariant,运行时检查) |
| 灵活性 | 需要预定义,不够灵活 | 动态绑定,非常灵活 |
| 性能 | 较高(直接调用或事件队列) | 中等(事件队列+回调) |
| 使用场景 | 固定的对象间通信 | 动态的、多对多通信 |
| 跨线程 | 自动处理(QueuedConnection) | 自动处理(AsyncReceiver) |
| 一对多 | 支持 | 支持 |
| 解耦程度 | 较高 | 极高(通过主题ID解耦) |
适用场景选择:
-
使用Qt信号槽:
- 对象间有明确的、固定的通信关系
- 需要编译时类型检查
- 性能要求高
-
使用发布订阅:
- 需要动态的、多对多通信
- 发布者和订阅者不需要知道对方
- 需要运行时灵活性
发布类型说明
1. Sync(同步)
在当前线程立即执行回调函数,阻塞直到回调完成。
cpp
pubSub->publish("topic1", data, PublishType::Sync);
// 回调函数立即执行,完成后继续
适用场景:
- 需要立即得到结果的场景
- 简单的数据处理
- 性能要求高的场景
2. AsyncCurrent(异步当前线程)
将回调放入当前线程的事件队列,在事件循环的下一次迭代中执行。
cpp
pubSub->publish("topic1", data, PublishType::AsyncCurrent);
// 立即返回,回调在事件循环中执行
适用场景:
- 需要延迟执行但仍在当前线程
- 避免在同一个调用栈中递归执行
- 批量操作后统一处理
3. AsyncReceiver(异步接收者)
将回调放入接收者对象所在线程的事件队列中执行。
cpp
pubSub->publish("topic1", data, PublishType::AsyncReceiver);
// 立即返回,回调在接收者线程的事件循环中执行
适用场景:
- 跨线程通信
- GUI更新(必须在主线程)
- 线程安全的异步操作
使用方法
基本使用
cpp
#include "eventpubsub.h"
// 获取单例实例
EventPubSub* pubSub = EventPubSub::instance();
// 创建接收者对象
QObject* receiver = new QObject();
// 订阅事件
pubSub->subscribe("topic1", receiver, [](const QVariant &data) {
qDebug() << "Received data:" << data;
});
// 发布消息
QVariant message = QString("Hello World");
pubSub->publish("topic1", message, PublishType::AsyncReceiver);
// 取消订阅
pubSub->unsubscribe("topic1", receiver);
订阅示例
cpp
// 方式1:使用lambda表达式
pubSub->subscribe("sensor/data", receiver, [](const QVariant &data) {
int value = data.toInt();
qDebug() << "Sensor value:" << value;
});
// 方式2:使用成员函数(需要包装)
class MyClass : public QObject {
Q_OBJECT
public:
void handleData(const QVariant &data) {
qDebug() << "Data received:" << data;
}
};
MyClass* obj = new MyClass();
pubSub->subscribe("topic1", obj, [obj](const QVariant &data) {
obj->handleData(data);
});
// 方式3:使用函数对象
struct DataHandler {
void operator()(const QVariant &data) {
qDebug() << "Handled:" << data;
}
};
pubSub->subscribe("topic1", receiver, DataHandler());
发布示例
cpp
// 发布字符串
pubSub->publish("message", QVariant("Hello"), PublishType::AsyncReceiver);
// 发布整数
pubSub->publish("counter", QVariant(42), PublishType::Sync);
// 发布自定义结构(需要注册元类型)
struct MyData {
int id;
QString name;
};
Q_DECLARE_METATYPE(MyData)
MyData data{1, "Test"};
pubSub->publish("custom", QVariant::fromValue(data), PublishType::AsyncReceiver);
跨线程使用
cpp
// 在工作线程中
class WorkerThread : public QThread {
protected:
void run() override {
EventPubSub* pubSub = EventPubSub::instance();
msleep(1000);
qWarning()<< "publish threadId:"<<QThread::currentThreadId();
// 发布消息(自动在接收者线程执行)
pubSub->publish("work/done", QVariant("Result"),
PublishType::AsyncReceiver);
}
};
// 在主线程中
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow() {
EventPubSub* pubSub = EventPubSub::instance();
// 订阅(回调在主线程执行)
pubSub->subscribe("work/done", this, [this](const QVariant &data) {
// 安全地更新GUI(在主线程中)
statusLabel->setText(data.toString());
qWarning()<< "recv threadId:"<<QThread::currentThreadId();
qWarning()<<"msg"<<data.toString();
});
// 启动工作线程
WorkerThread* worker = new WorkerThread();
worker->start();
}
};
API参考
EventPubSub类
静态方法
cpp
static EventPubSub* instance()
获取单例实例。
订阅方法
cpp
bool subscribe(const QString &topicId,
QObject *receiver,
std::function<void(const QVariant &)> callback)
订阅事件。
参数:
topicId:主题ID(消息ID)receiver:订阅者对象指针callback:回调函数
返回:订阅是否成功
取消订阅方法
cpp
bool unsubscribe(const QString &topicId, QObject *receiver)
取消订阅指定主题。
cpp
int unsubscribeAll(QObject *receiver)
取消某个对象的所有订阅。
发布方法
cpp
void publish(const QString &topicId,
const QVariant &data,
PublishType type = PublishType::AsyncReceiver)
发布消息。
参数:
topicId:主题IDdata:消息数据type:发布类型(默认异步接收者)
查询方法
cpp
int getSubscriberCount(const QString &topicId) const
获取某个主题的订阅者数量。
cpp
QStringList getAllTopics() const
获取所有主题ID列表。
cpp
void clearAll()
清空所有订阅。
构建说明
使用CMake
bash
mkdir build
cd build
cmake ..
make
集成到项目
将以下文件添加到你的项目:
eventpubsub.heventpubsub.cpp
在CMakeLists.txt中添加:
cmake
add_subdirectory(QtEventPubSub)
target_link_libraries(your_target QtEventPubSub)
或在.pro文件中:
pro
SOURCES += QtEventPubSub/eventpubsub.cpp
HEADERS += QtEventPubSub/eventpubsub.h
注意事项
- 对象生命周期:确保订阅者对象在回调执行时仍然存在,否则可能导致崩溃
- 线程安全:虽然系统本身是线程安全的,但回调函数中的操作需要自行保证线程安全
- 性能考虑:同步模式性能最好,但会阻塞;异步模式不阻塞,但有一定开销
- 内存管理:组件内部使用辅助对象(ReceiverHelper)管理回调,这些对象不设置父对象,由组件自动管理生命周期
- 事件循环:异步模式需要目标线程有事件循环在运行
- 辅助对象设计:每个接收者会创建一个ReceiverHelper辅助对象,该对象不设置父对象(使用nullptr),以避免跨线程父子关系问题。这是Qt对象线程安全的重要设计决策
与ROS消息系统的对比
| 特性 | ROS | QtEventPubSub |
|---|---|---|
| 消息类型 | 强类型(.msg文件) | 弱类型(QVariant) |
| 线程安全 | 是 | 是 |
| 发布模式 | 异步 | 同步/异步可选 |
| 跨进程 | 支持 | 不支持(单进程) |
| 依赖 | ROS核心 | Qt Core |
| 性能 | 中等 | 高(单进程) |
最佳实践
- 使用有意义的主题ID :使用命名空间风格,如
"sensor/temperature"、"control/motor" - 合理选择发布类型 :根据场景选择合适的发布模式
- 同线程快速调用:使用
Sync - 需要延迟但仍在当前线程:使用
AsyncCurrent - 跨线程通信:使用
AsyncReceiver(默认)
- 同线程快速调用:使用
- 及时取消订阅 :对象销毁前取消订阅,避免内存泄漏
- 在对象的析构函数中调用
unsubscribeAll() - 或使用
QObject::destroyed信号自动清理
- 在对象的析构函数中调用
- 错误处理:在回调函数中添加错误处理逻辑
- 性能优化:对于高频消息,考虑批量处理或使用同步模式
- 与Qt信号槽的选择 :
- 固定的一对一通信:优先使用Qt信号槽(类型安全、性能好)
- 动态的多对多通信:使用发布订阅(灵活、解耦)
- 需要运行时绑定:使用发布订阅
- 需要编译时类型检查:使用Qt信号槽
- 线程安全注意事项 :
- 组件本身是线程安全的
- 但回调函数中的操作需要自行保证线程安全
- 特别是访问共享数据时,需要额外的同步机制
示例场景
场景1:传感器数据订阅
cpp
// 传感器发布数据
EventPubSub* pubSub = EventPubSub::instance();
pubSub->publish("sensor/temperature", QVariant(25.5),
PublishType::AsyncReceiver);
// 多个订阅者订阅
pubSub->subscribe("sensor/temperature", displayWidget,
[](const QVariant &data) {
// 更新显示
});
pubSub->subscribe("sensor/temperature", logger,
[](const QVariant &data) {
// 记录日志
});
场景2:命令发布
cpp
// 发布命令
pubSub->publish("command/move", QVariant(QPoint(100, 200)),
PublishType::Sync);
// 订阅命令
pubSub->subscribe("command/move", robotController,
[](const QVariant &data) {
QPoint pos = data.toPoint();
// 执行移动命令
});
场景3:状态通知
cpp
// 状态变化通知
pubSub->publish("status/connected", QVariant(true),
PublishType::AsyncReceiver);
// 订阅状态变化
pubSub->subscribe("status/connected", statusBar,
[](const QVariant &data) {
bool connected = data.toBool();
// 更新状态栏
});
许可证
本项目采用MIT许可证。
贡献
欢迎提交Issue和Pull Request。
更新日志
v1.0.0
- 初始版本
- 实现基本的发布订阅功能
- 支持三种发布模式
- 线程安全实现