概述
介绍说明
学习 QT 的时候,觉得 QT 提供的信号槽机制非常有用,这种机制可以解决对象与对象之间通信中的耦合问题,原本想从网络上了解一下具体的实现思路用于项目中,意外的发现了用 C++ 实现的信号槽开源库 - “sigslot” 。它仅有一个 sigslot.h 源文件,简单而又满足了我想将这种机制应用到项目中的想法。
从官方网了解到,开发者可能是一位女程序员,她原本使用 MFC 开发,后接触到 QT ,被 QT 的信号槽机制惊艳到了后自行设计类似 QT 信号槽机制的开源库 “sigslot ”。文中说曾被 MFC 逼的想放弃编程,哈哈。
官方网:http://sigslot.sourceforge.net/ 说明文档:http://sigslot.sourceforge.net/sigslot.pdf
使用例程
我的习惯是先学会如何使用,通过使用来发现更多的疑问,再通过阅读文档和源码来寻找答案,最终达到可以灵活应用的目的。
以下示例演示了如何通过 sigslot 的信号槽机制,将网络状态变化实时的通知给负责提示音以及动画显示的类,便于进行指定提示音播报和UI显示。
NetworkManager 为信号发生者,Tips、Display 属于信号接收者(即槽)。
#include <iostream>
#include <thread>
#include "sigslot.h"
using namespace std;
using namespace sigslot;
// 负责网络管理的类
class NetworkManager
{
public:
// 定义三个信号
signal1<string> sigConnecting; // 正在连接的信号
signal0<> sigConnected; // 连接成功的信号
signal0<> sigDisConnect; // 断开连接的信号
public:
bool run()
{
// 模拟正在连接 WIFI 的情况
printf("connect wifi:%s...n", "lovemengx");
sigConnecting.emit("lovemengx");
puts("");
this_thread::sleep_for(chrono::seconds(1));
// 模拟已经成功连接 WIFI 的情况
printf("connect success...n");
sigConnected.emit();
puts("");
this_thread::sleep_for(chrono::seconds(1));
// 模拟已经断开连接 WIFI 的情况
printf("disconnect...n");
sigDisConnect();
puts("");
this_thread::sleep_for(chrono::seconds(1));
}
};
// 负责播报提示音的类(槽)
class Tips : public has_slots<>
{
public:
void onPlayNetworkConnecting(string ssid)
{
cout << "tips:onPlayNetworkConnecting(" << ssid << ") ..." << endl;
}
void onPlayNetworkConnected()
{
cout << "tips:onPlayNetworkConnected ..." << endl;
}
void onPlayNetworkDisConnect()
{
cout << "tips:onPlayNetworkDisConnect ..." << endl;
}
};
// 负责显示的类(如UI、动画等等)(槽)
class Display : public has_slots<>
{
public:
void onShowNetworkConnecting(string ssid)
{
cout << "show:onShowNetworkConnecting(" << ssid << ") ..." << endl;
}
void onShowNetworkConnected()
{
cout << "show:onShowNetworkConnected ..." << endl;
}
void onShowNetworkDisConnect()
{
cout << "show:onShowNetworkDisConnect ..." << endl;
}
};
int main()
{
NetworkManager mNetworkManager;
Display mDisplay;
Tips mTips;
puts("-----------------------------------------------------------");
// 将网络状态与提示音类进行关联
mNetworkManager.sigConnecting.connect(&mTips, &Tips::onPlayNetworkConnecting);
mNetworkManager.sigConnected.connect(&mTips, &Tips::onPlayNetworkConnected);
mNetworkManager.sigDisConnect.connect(&mTips, &Tips::onPlayNetworkDisConnect);
// 将网络状态与显示类进行关联
mNetworkManager.sigConnecting.connect(&mDisplay, &Display::onShowNetworkConnecting);
mNetworkManager.sigConnected.connect(&mDisplay, &Display::onShowNetworkConnected);
mNetworkManager.sigDisConnect.connect(&mDisplay, &Display::onShowNetworkDisConnect);
// 模拟网络状态变化
mNetworkManager.run();
puts("-----------------------------------------------------------");
// 如果不需要正在连接网络的提示音, 可以单独断开该信号
printf("sigConnecting.disconnect -> mTipsn");
mNetworkManager.sigConnecting.disconnect(&mTips);
// 模拟网络状态变化
mNetworkManager.run();
puts("-----------------------------------------------------------");
// 在某些需要安静的场景, 希望屏蔽声音提示, 那么可以将提示音与网络状态信号断开
printf("mTips.disconnect_all()...n");
mTips.disconnect_all();
// 模拟网络状态变化
mNetworkManager.run();
puts("-----------------------------------------------------------");
return 0;
}
线程安全问题
线程锁实现方式
// 当定义了 SIGSLOT_PURE_ISO 或者 没有定义 WIN32__GNUG__SIGSLOT_USE_POSIX_THREADS 时, 定义 _SIGSLOT_SINGLE_THREADED(即不使用任何锁保护)
#if defined(SIGSLOT_PURE_ISO) || (!defined(WIN32) && !defined(__GNUG__) && !defined(SIGSLOT_USE_POSIX_THREADS))
# define _SIGSLOT_SINGLE_THREADED
#elif defined(WIN32) // 如果定义了 WIN32, 说明是 WIN32 平台
# define _SIGSLOT_HAS_WIN32_THREADS // 定义 _SIGSLOT_HAS_WIN32_THREADS 宏, 用于启用 WIN32 平台下的线程锁 API 来实现线程安全机制
# if !defined(WIN32_LEAN_AND_MEAN)
# define WIN32_LEAN_AND_MEAN
# endif
# include <windows.h> // 并且包含 windows.h 头文件
#elif defined(__GNUG__) || defined(SIGSLOT_USE_POSIX_THREADS) // 如果定义了 __GNUG__ 或者 SIGSLOT_USE_POSIX_THREADS, 则使用 POSIX 线程锁 API 来实现线程安全机制
# define _SIGSLOT_HAS_POSIX_THREADS
# include <pthread.h> // 包含 phread.h 头文件
#else
# define _SIGSLOT_SINGLE_THREADED // 如果上方没有一个满足, 则定义 _SIGSLOT_SINGLE_THREADED(即不使用任何锁保护)
#endif
上面从 sigslot.h 中摘抄下来宏代码,并加入了对应的注释,主要分为两类:可强制设置、自动适应平台。
可强制性设置:
- SIGSLOT_PURE_ISO:强制设定其为ISO C++编译器,并关闭所有的操作系统提供的线程安全的支持。
- SIGSLOT_USE_POSIX_THREADS:在使用非C++编译器时,并且编译器支持Posix时,强制使用Posix线程支持。
自动适应平台:
- __GNUG__:当有此宏定义时,意味着该平台支持 POSIX 线程,将会自动启用 POSIX 线程部分的代码
- WIN32:当有此宏定义时,意味着是 Windows 平台,需要使用 Windows 下的线程锁API接口。
- 其他:如果上方两个宏定义都不存在,则默认关闭所有线程安全支持,可以通过上述 “可强制设置”的方式进行强制设置。
一般在 Linux 平台和 Windows 平台下使用,不需要去关心上面的宏定义,因为源码中已经会自动适应当前平台。
无线程保护的线程锁的代码实现:
class single_threaded
{
public:
single_threaded()
{
;
}
virtual ~single_threaded()
{
;
}
virtual void lock()
{
;
}
virtual void unlock()
{
;
}
};
WIN32 平台下的线程锁实现 :
#ifdef _SIGSLOT_HAS_WIN32_THREADS
// The multi threading policies only get compiled in if they are enabled.
class multi_threaded_global
{
public:
multi_threaded_global()
{
static bool isinitialised = false;
if (!isinitialised)
{
InitializeCriticalSection(get_critsec());
isinitialised = true;
}
}
multi_threaded_global(const multi_threaded_global&)
{
;
}
virtual ~multi_threaded_global()
{
;
}
virtual void lock()
{
EnterCriticalSection(get_critsec());
}
virtual void unlock()
{
LeaveCriticalSection(get_critsec());
}
private:
CRITICAL_SECTION* get_critsec()
{
static CRITICAL_SECTION g_critsec;
return &g_critsec;
}
};
class multi_threaded_local
{
public:
multi_threaded_local()
{
InitializeCriticalSection(&m_critsec);
}
multi_threaded_local(const multi_threaded_local&)
{
InitializeCriticalSection(&m_critsec);
}
virtual ~multi_threaded_local()
{
DeleteCriticalSection(&m_critsec);
}
virtual void lock()
{
EnterCriticalSection(&m_critsec);
}
virtual void unlock()
{
LeaveCriticalSection(&m_critsec);
}
private:
CRITICAL_SECTION m_critsec;
};
#endif // _SIGSLOT_HAS_WIN32_THREADS
POSIX 线程锁的代码实现:
#ifdef _SIGSLOT_HAS_POSIX_THREADS
// The multi threading policies only get compiled in if they are enabled.
class multi_threaded_global
{
public:
multi_threaded_global()
{
pthread_mutex_init(get_mutex(), NULL);
}
multi_threaded_global(const multi_threaded_global&)
{
;
}
virtual ~multi_threaded_global()
{
;
}
virtual void lock()
{
pthread_mutex_lock(get_mutex());
}
virtual void unlock()
{
pthread_mutex_unlock(get_mutex());
}
private:
pthread_mutex_t* get_mutex()
{
static pthread_mutex_t g_mutex;
return &g_mutex;
}
};
class multi_threaded_local
{
public:
multi_threaded_local()
{
pthread_mutex_init(&m_mutex, NULL);
}
multi_threaded_local(const multi_threaded_local&)
{
pthread_mutex_init(&m_mutex, NULL);
}
virtual ~multi_threaded_local()
{
pthread_mutex_destroy(&m_mutex);
}
virtual void lock()
{
pthread_mutex_lock(&m_mutex);
}
virtual void unlock()
{
pthread_mutex_unlock(&m_mutex);
}
private:
pthread_mutex_t m_mutex;
};
#endif // _SIGSLOT_HAS_POSIX_THREADS
线程安全保护
- single_threaded:
单线程模型,不需要考虑并发等情况,也就不需要线程保护机制了。所有的信号槽的操作都在一个线程中调用。
- multi_threaded_global:
全局多线程模型,适用于多线程并发的情况,所有信号槽的操作都将由一个全局的线程锁保护,资源占用更少,但也容易出现某个信号会因为其他信号产生资源竞争而形成阻塞等待的情况(时间会较长)。
- multi_threaded_local:
本地多线程模型,同样适用于多线程并发的情况,每个信号槽都拥有各自的线程所保护,好处是只有该信号自身出现资源竞争才会形成阻塞等待的情况(时间相对全局来说短很多),但也同时占用系统资源更多,可能会影响操作系统的运行速度。
全局多线程模型其实是使用了一个静态变量来实现全局共享一把锁进行线程保护:(因为静态变量是所有类对象共同拥有的)
class multi_threaded_global
{
public:
multi_threaded_global()
{
pthread_mutex_init(get_mutex(), NULL); // 取出静态互斥锁进行初始化
}
...
virtual void lock()
{
pthread_mutex_lock(get_mutex()); // 通过静态互斥锁进行锁保护
}
virtual void unlock()
{
pthread_mutex_unlock(get_mutex()); // 通过静态互斥锁进行锁解除
}
private:
pthread_mutex_t* get_mutex()
{
static pthread_mutex_t g_mutex; // 定义了静态的互斥锁
return &g_mutex;
}
};
本地多线程模型则使用的是自动变量,每次创建新的对象时,都会拥有一个新的互斥锁:
class multi_threaded_local
{
public:
multi_threaded_local()
{
pthread_mutex_init(&m_mutex, NULL);
}
multi_threaded_local(const multi_threaded_local&)
{
pthread_mutex_init(&m_mutex, NULL);
}
virtual ~multi_threaded_local()
{
pthread_mutex_destroy(&m_mutex);
}
virtual void lock()
{
pthread_mutex_lock(&m_mutex);
}
virtual void unlock()
{
pthread_mutex_unlock(&m_mutex);
}
private:
pthread_mutex_t m_mutex; // 自动变量
};
线程模型的使用方法:
- 隐式指定
// 这里定义了该宏,并且指定为 single_threaded 模型 #define SIGSLOT_DEFAULT_MT_POLICY single_threaded // 如果没有定义默认线程保护方式 #ifndef SIGSLOT_DEFAULT_MT_POLICY # ifdef _SIGSLOT_SINGLE_THREADED // 如果定义了不使用任何锁保护 # define SIGSLOT_DEFAULT_MT_POLICY single_threaded // 则默认为不使用任何锁保护 # else # define SIGSLOT_DEFAULT_MT_POLICY multi_threaded_local // 否则默认以 multi_threaded_local 方式进行保护 # endif #endif
// 在信号定义,这里默认指定了 SIGSLOT_DEFAULT_MT_POLICY template<class mt_policy = SIGSLOT_DEFAULT_MT_POLICY> class signal0 : public _signal_base0<mt_policy> { public: typedef _signal_base0<mt_policy> base; ... ... ... };
// 在槽定义,这里默认指定了 SIGSLOT_DEFAULT_MT_POLICY template<class mt_policy = SIGSLOT_DEFAULT_MT_POLICY> class has_slots : public has_slots_interface, public mt_policy { private: typedef std::set<_signal_base_interface*> sender_set; typedef sender_set::const_iterator const_iterator; public: ... ... has_slots(const has_slots& hs) { lock_block<mt_policy> lock(this); ... ... } };
在定义信号与槽时,不指定线程模型,可以通过定义宏 SIGSLOT_DEFAULT_MT_POLICY 来默认指定某一个线程模型。
- 显式指定
通过上面也可知道,可以通过定义信号以及继承槽时,指定线程模型。// 定义三个信号 signal1<string, multi_threaded_local> sigConnecting; // 正在连接的信号 signal0<multi_threaded_local> sigConnected; // 连接成功的信号 signal0<multi_threaded_local> sigDisConnect; // 断开连接的信号
// 负责播报提示音的类(槽) class Tips : public has_slots<multi_threaded_local> { public: ... }; // 负责显示的类(如UI、动画等等)(槽) class Display : public has_slots<multi_threaded_local> { public: ... };
以上显式设置信号和槽都以本地多线程模型来作为线程保护机制:multi_threaded_local
对象拷贝问题
在文档中,作者有提到对象拷贝的实现方式,但通过阅读源码,发现并没有重载赋值运算符的实现,因此特地写了一个代码进行试验,发现确实会出现问题。
int main()
{
signal0<multi_threaded_local> sigConnected1;
signal0<multi_threaded_local> sigConnected2;
Display mDisplay;
Tips mTips;
sigConnected1.connect(&mTips, &Tips::onPlayNetworkConnected);
sigConnected1.connect(&mDisplay, &Display::onShowNetworkConnected);
printf("sigConnected1.emit() start...n");
sigConnected1.emit();
printf("sigConnected1.emit() end...n");
puts("-----------------------------------------------------------");
printf("sigConnected2 = sigConnected1n");
sigConnected2 = sigConnected1;
printf("sigConnected2.emit() start...n");
sigConnected2.emit();
printf("sigConnected2.emit() end...n");
puts("-----------------------------------------------------------");
printf("sigConnected1.disconnect(&mTips)n");
sigConnected1.disconnect(&mTips);
printf("sigConnected1.emit() start...n");
sigConnected1.emit();
printf("sigConnected1.emit() end...n");
puts("-----------------------------------------------------------");
printf("sigConnected2.emit() start...n");
sigConnected2.emit(); // 这里会出现异常, 如果是在 Linux 则会报段错误
printf("sigConnected2.emit() end...n");
puts("-----------------------------------------------------------");
return 0;
}
通过实际测试可以发现,在信号1 sigConnected1.disconnect(&mTips); 之后,信号1 是正常的,但是信号2在触发时,就出现了问题。
通过分析代码,当我们调用 connect 进行信号关联时会 new 一个新的对象加入到 list 中,而调用 disconnect 解除关联时,会 delete 该对象。
template<class desttype>
void connect(desttype* pclass, void (desttype::* pmemfun)())
{
lock_block<mt_policy> lock(this);
_connection0<desttype, mt_policy>* conn =
new _connection0<desttype, mt_policy>(pclass, pmemfun); // new 了一个对象
m_connected_slots.push_back(conn); // 将对象地址存入
pclass->signal_connect(this);
}
void disconnect(has_slots_interface* pclass)
{
lock_block<mt_policy> lock(this);
typename connections_list::iterator it = m_connected_slots.begin();
typename connections_list::iterator itEnd = m_connected_slots.end();
while (it != itEnd)
{
if ((*it)->getdest() == pclass)
{
delete* it; // 释放内存
m_connected_slots.erase(it); // 从 list 中移除该项
pclass->signal_disconnect(this);
return;
}
++it;
}
}
由于没有重载赋值运算符,编译器将会自动生成赋值运算符相关的代码,只做到了浅拷贝,因此我们把信号1赋值给信号2,只是拷贝了 list 里面的数据,即 sigConnected1.connect() 时 new 的地址。当调用 sigConnected1.disconnect() 时,就会 delete 该对象,但是 sigConnected2 里面的 list 却仍保留着该对象地址,所以在调用 sigConnected2.emit(); 就会去访问已经被释放了的对象,从而产生错误。
解决方法就是修改 sigslot 库,为每一个信号实现深拷贝,如不带参数的 signal0:
// add lmx: https://me.csdn.net/lovemengx -- 2020-04-12
signal0 &operator=(const signal0 &singnal)
{
if (this != &singnal)
{
lock_block<mt_policy> lock(this);
typename connections_list::const_iterator itNext, it = singnal.m_connected_slots.begin();
typename connections_list::const_iterator itEnd = singnal.m_connected_slots.end();
while (it != itEnd)
{
this->m_connected_slots.push_back((*it)->clone());
(*it)->getdest()->signal_connect(this);
itNext = it;
++itNext;
it = itNext;
}
}
return *this;
}
使用修改之后的执行效果:
最后说明
- 从官方下载的源代码,编译有问题,在网上找到一份据说是从 WebRtc 开源库中取出来的,但同样存在对象拷贝问题。
- 该开源库支持最多 8 个参数的信号定义,不够可以自行扩展,不过一般一个参数就够了,传一个结构体或者对象地址
- 信号与槽函数的对应,只和参数类型、参数个数有关,如果一致,那么就可以进行关联。
- 槽函数的返回值需是 void 类型,且槽函数所属的类必须继承 has_slots<>,如果需要判断返回值可以自行实现。
- 槽函数的调用顺序与连接信号顺序一致,因为采用的是 list 实现,所以没有优先级之分,不过可以自己修改实现。
- 该开源库属于同步通知类型,即一个信号对应多个槽函数,只要有一个槽函数阻塞,那么后面的槽函数就无法被调用,信号调用的.emit() 也会阻塞。这点,可以通过查看 emit 的实现即可确认。
- 经过修改完善后的源代码下载连接(经过连续几个小时的测试,未出现内存泄漏问题,Windows、Ubuntu 可直接编译):https://download.csdn.net/download/lovemengx/12324035
最后
以上就是动听电灯胆为你收集整理的【学习笔记】开源库之 - sigslot (提供该库存在对象拷贝崩溃问题的解决方案)的全部内容,希望文章能够帮你解决【学习笔记】开源库之 - sigslot (提供该库存在对象拷贝崩溃问题的解决方案)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复