Home

cuipf blog

18 Oct 2017

sigslot的使用和分析

基本使用

实例

先看实例:

class Light
{
public:
	Light(bool state) 
	{ 
		state_ = state; 
		Displaystate(); 
	}

	void ToggleState() 
	{ 
		state_ = !state_; 
		Displaystate(); 
	}

	void TurnOn() 
	{ 
		state_ = true; 
		Displaystate(); 
	}

	void TurnOff() 
	{ 
		state_ = false; 
		Displaystate(); 
	}

	void Displaystate() 
	{ 
		std::cout << "The state is " << (state_ ? 1:0) << std::endl; 
	}

private:
	bool state_;
};

class ComSwitch
{
public:
	virtual void Clicked() = 0;
};


class ToggleSwitch : public ComSwitch
{
public:
	ToggleSwitch(Light& lp) : lp_(lp)
	{

	}
	virtual void Clicked()
	{
		lp_.ToggleState();
	}
private:
	Light& lp_;
};

Light lp1(true), lp2(false);
ToggleSwitch csw1(lp1);
ToggleSwitch csw2(lp2);
csw1.Clicked();
csw1.Clicked();
csw2.Clicked();
csw2.Clicked();

例子非常简单, 对家庭灯,开关这一套系统的一个简单封装,ToggleSwitch类和Light类相互关联是通过类的成员函数相互调用实现;如果项目足够大,相互关联的类越来越多, 使用这种方式就不得不考虑一下几个问题?

  • 各个类耦合大;
  • 对象生命周期的问题;
  • 多线程情况下是否安全等;
  • 如果一个控制类要控制多个对象操作, 实现起来比较麻烦; 一个更好的解决方式是使用信号的机制,通过这种方式可以实现模块之间的高内聚,低耦合;不用去关心太多类之间联系的细节;
class Light : public sigslot::has_slots<>
{
public:
	Light(bool state) 
	{ 
		state_ = state; 
		Displaystate(); 
	}

	void ToggleState() 
	{ 
		state_ = !state_; 
		Displaystate(); 
	}

	void TurnOn() 
	{ 
		state_ = true; 
		Displaystate(); 
	}

	void TurnOff() 
	{ 
		state_ = false; 
		Displaystate(); 
	}

	void Displaystate() 
	{ 
		std::cout << "The state is " << (state_ ? 1:0) << std::endl; 
	}

private:
	bool state_;
};

class Switch
{
public:
	sigslot::signal0<> Clicked;
};

Switch sw1, sw2, all_on, all_off;
sw1.Clicked.connect(&lp1, &Light::ToggleState); //绑定
sw2.Clicked.connect(&lp2, &Light::ToggleState);
all_on.Clicked.connect(&lp1, &Light::TurnOn);
all_on.Clicked.connect(&lp2, &Light::TurnOn);
std::cout << "all on clicked off.." << std::endl;
all_on.Clicked.connect(&lp1, &Light::TurnOff);
all_off.Clicked.connect(&lp1, &Light::TurnOff);
all_off.Clicked.connect(&lp2, &Light::TurnOff);
std::cout << "sw1 sw2 clicked..." << std::endl;
sw1.Clicked();
sw2.Clicked();
std::cout << "all on clicked..." << std::endl;
all_on.Clicked();
std::cout << "all off clicked..." << std::endl;
all_off.Clicked();
sw1.Clicked.disconnect(&lp1);
sw2.Clicked.disconnect(&lp2);
all_on.Clicked.disconnect(&lp1);
all_on.Clicked.disconnect(&lp2);
all_off.Clicked.disconnect(&lp1);
all_off.Clicked.disconnect(&lp2);

与上例相比最大的改变是,Clicked不再是一个纯虚函数,而是一个信号; 类Light则需要继承has_slots; Switch类和Light 关联也变得非常清晰;该方式也不用关心关联对象生命周期的问题(后续分析源代码详细介绍)。

说明

  • sigslot所有的定义都包含在sigslot命名空间下;
  • 需包含头文件**#include **;

参数类型

signal和slot均可以携带一到八个自定义类型参数;不同参数信号声明方式如下:


signaln<type1, type2...> Clicked;

其中n表示参数个数; 实例:

class Window
{
public:
    enum WindowState { Minimised, Normal, Maximised };
    signal1<WindowState> StateChanged;
    signal2<int, int> MovedTo;
    signal2<int, int> Resized;
};

class MyControl : public Control, public has_slots<>
{
public:
    void OnStateChanged(WindowState ws);
    void OnMovedTo(int x, int y);
    void OnResize(int x, int y);
};
Window w;
MyControl c;
w.StateChanged.connect(&c, &MyControl::OnStateChanged);
w.MovedTo.connect(&c, &MyControl::OnMovedTo);
w.Resized.connect(&c, &MyControl::OnResize);

唯一值得注意的就是信号和插槽在连接时参数要保持一致。

多线程

介绍

sigslot实现仅仅用到ISO C++和C++标准库(STD), 所以可以在多平台使用; 至少在单线程模式下无需做任何修改, 多线程每个平台不太一样; 多线程支持, 在win32下需要头文件windows.h,或者需要pthread.h头文件在支持Posix Threads的系统下;

sigslot三种线程策略如下:

  1. Single Threaded 单线程模式,无需加锁;
  2. Multithreaded Global 多线程模式,全局锁;使用全局锁保护sigslot中间产生的数据;这种模式开销小,但有时会出现不必要的阻塞;
  3. Multithreaded Local 多线程,局部锁;也以为着每一个signal有自己的锁;只有在非常必须的情况下,才会加锁;代价就是会创建和维护大量的锁;

多线程设置

/*
    1. signal
*/
// Single-threaded
signal1<int, single_threaded> Sig1;

// Multithreaded Global
signal1<int, multi_threaded_global> Sig2;

// Multithreaded Local
signal1<int, multi_threaded_local> Sig3

/*
    2. has_slots
*/
//single_threaded 也可以是multi_threaded_global或者multi_threaded_local
class Control : public has_slots<single_threaded>
{

};

class ControlMt : public has_slots<multi_threaded_local>
{
public:
    void Entry1() // Slot
    {
        lock();
        ...
        unlock();
    }
    void Entry2() // Slot
    {
        lock();
        ...
        unlock();
    }
};

class ControlMt : public has_slots<multi_threaded_local>
{
public:
    void Entry1() // Slot
    {
        lock_block<multi_threaded_local> lock(this);
        ...
    }
    void Entry2()
    {
        lock_block<multi_threaded_local> lock(this);
        ...
    }
};

虽然sigslot不是一个完整的线程库,但也提供完成线程安全的函数如上所示;

实现

类图

sigslots类图

类说明

has_slots

该类是所有处理对象的基类,处理对象中每一个处理函数就相当于一个slot(槽), has_slots提供了信号的管理,管理该对象槽相关的所有信号; 对外该类提供一下几个接口:

  • signal_connect 添加信号指针;
  • signal_disconnect 删除指定信号;
  • disconnect_all 删除与该对象相关的所有信号;删除信号之前会依次通知每一个信号切断与该类的关联;

还需要注意,has_slots还提供了拷贝构造函数, 拷贝构造不仅仅拷贝了信号集合, 而且也会拷贝与之信号集合中每个信号有关的其他处理对象(也就是”_connection_base0”集合);

_signal_base(n)

_signal_base 代表一个信号, 其中包含信号处理对象的链表, 完成信号处理添加, 调用等各种操作; 其后面的n代表函数可以有n个参数(1 - 8); 分别对应signal1-signal8; _signal_base处理对象链表; 每一个结点其实就是一个_connection_base0的指针; _connection_base0外部不感知;生命周期均有signal控制.

接口说明:

  • _signal_base(n)拷贝构造函数: 信号的拷贝不是简单地对被拷贝者的list链表的拷贝, 它的机制是遍历list链表克隆其中有效的处理者,将其加入到自己的 list 链表中, 并将信号添加到处理者对象的集合中。
  • _signal_base(n)析构函数: 清空list链表,并释放链表节点中指针的内存(_connection_base0指针),若处理者有效,通知其处理者对象(has_slot), 删掉该信号。
  • is_empty: 清除无效处理对象; 判断信号处理对象是否为空;
  • persist_disconnect_all: 清空列表, 释放内存;通知处理者,删除该信号;
  • disconnect_all: 和 persist_disconnect_all相比, 没有做_connection_base0指针指向内存释放以及list的清除;
  • disconnect: 标记指定处理对象为无效, 并在处理对象的信号集合中删除该信号;
  • slot_disconnect: 基类接口, 解除指定处理者关联; 实际效果是信号的对应处理者被置位无效;
  • slot_duplicate: 基类接口, 当oldtarget被拷贝时调用, 使得在oldtarget中的信号,增加处理者,新增加的处理者指向newtarget.

_connection_base(n)

_connection_base是一个信号的处理者链表的一个节点对象,纯虚类,内部只有一个有效状态标志, 从上面的分析中已经知道,在信号调用 disconnect, 或者 has_slots 被析构时,这个状态位变为 无效,此后信号即使被触发,也不会调用处理对象。

总结

sigslot功能和boost库中的信号Signals作用差不多;

sigslot优点

  • 不用担心空回调,当回调对象析构时会自动disconnect;
  • 支持多线程,线程安全,有锁;

sigslot缺点

  • 只能回调void类型函数,不支持返回值; boost中的signals库架构类似,支持返回值;
  • slot没有优先级,不能动态调整回调队列中的先后顺序;

sigslot限制

slot函数(被回调的函数)就是普通的成员函数,但有以下限制:

  • 返回值必须为void;
  • Slot参数个数范围为0-8个(可扩展);
  • 实现slot的类必须继承自has_slots<>;

cuipf

scribble