学习 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
#include
#include "sigslot.h"
using namespace std;
using namespace sigslot;
// 负责网络管理的类
class NetworkManager
{
public:
// 定义三个信号
signal1 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 -> mTips\n");
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 头文件
#elif defined(__GNUG__) || defined(SIGSLOT_USE_POSIX_THREADS) // 如果定义了 __GNUG__ 或者 SIGSLOT_USE_POSIX_THREADS, 则使用 POSIX 线程锁 API 来实现线程安全机制
# define _SIGSLOT_HAS_POSIX_THREADS
# include // 包含 phread.h 头文件
#else
# define _SIGSLOT_SINGLE_THREADED // 如果上方没有一个满足, 则定义 _SIGSLOT_SINGLE_THREADED(即不使用任何锁保护)
#endif
上面从 sigslot.h 中摘抄下来宏代码,并加入了对应的注释,主要分为两类:可强制设置、自动适应平台。
可强制性设置:
自动适应平台:
一般在 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
全局多线程模型其实是使用了一个静态变量来实现全局共享一把锁进行线程保护:(因为静态变量是所有类对象共同拥有的)
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 signal0 : public _signal_base0
{
public:
typedef _signal_base0 base;
...
...
...
};
// 在槽定义,这里默认指定了 SIGSLOT_DEFAULT_MT_POLICY
template
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 lock(this);
...
...
}
};
在定义信号与槽时,不指定线程模型,可以通过定义宏 SIGSLOT_DEFAULT_MT_POLICY 来默认指定某一个线程模型。
// 定义三个信号
signal1 sigConnecting; // 正在连接的信号
signal0 sigConnected; // 连接成功的信号
signal0 sigDisConnect; // 断开连接的信号
// 负责播报提示音的类(槽)
class Tips : public has_slots
{
public:
...
};
// 负责显示的类(如UI、动画等等)(槽)
class Display : public has_slots
{
public:
...
};
以上显式设置信号和槽都以本地多线程模型来作为线程保护机制:multi_threaded_local
在文档中,作者有提到对象拷贝的实现方式,但通过阅读源码,发现并没有重载赋值运算符的实现,因此特地写了一个代码进行试验,发现确实会出现问题。
int main()
{
signal0 sigConnected1;
signal0 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 = sigConnected1\n");
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
void connect(desttype* pclass, void (desttype::* pmemfun)())
{
lock_block lock(this);
_connection0* conn =
new _connection0(pclass, pmemfun); // new 了一个对象
m_connected_slots.push_back(conn); // 将对象地址存入
pclass->signal_connect(this);
}
void disconnect(has_slots_interface* pclass)
{
lock_block 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 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;
}
使用修改之后的执行效果: