一、概述
Boost.Signals2库一个信号和槽管理系统的实现。信号表示带有多个目标的回调,在类似的系统中也成为发布者或者事件。信号被连接到一些槽上,这些槽是回调接收者(也成为事件目标或者订阅方),当信号被“发射”时调用。
信号和槽是受管理的,在这种情况下,信号和槽(或者更确切的说,作为槽的一部分出现的对象)可以跟踪连接,并且能够在其中一方被销毁时自动断开信号/槽连接。这使得用户能够创建信号/槽连接,不需要花费大力气来管理这些连接的生命周期,这些生命周期与所有涉及对象的生命周期有关。
当信号连接到多个槽时,有一个关于槽返回值和信号返回值之间关系的问题。Boost.Signals2允许用户指定组合多个返回值的方式。
不同于初版的Boost.Signals库,Boost.Signals2当前只有头文件,要使用库,只需要代码中包含头文件:#include <boost/signals2.hpp>
二、线程安全
信号库可以在多线程环境中安全的使用。这主要是通过对原始库的两个改变来实现的:一个是引入了新的依赖于shared_ptr
和weak_ptr
的自动连接管理方案,第二个变化时再signal
类中引入了一个互斥模板类型参数。
1、信号和组合器
每个信号对象默认构造一个互斥对象来保护其内部状态。此外,每次将一个新槽连接到信号时,都会创建一个互斥锁,以保护相关联的信号槽连接。
当信号的任意方法被调用时,信号的互斥锁都会被自动锁定。互斥锁通常会一直保持到方法调用结束,但是这个规则有一个主要的例外。当通过调用signal::operator()
调用一个信号时,调用首先获得该信号的互斥锁。然后它获得信号的槽列表的句柄,以及组合器的句柄。接下来,在调用组合器遍历槽列表之前,释放信号的互斥锁。因此,当槽执行时,信号不持有互斥锁。这种设计选择,使得槽中运行的代码不可能由于Boost.Signals2库内部使用任意互斥而导致死锁。它还可以防止槽由于意外而导致对库内部互斥递归加锁的情况。因此,如果在多个线程并发地调用一个信号,则可能并发地调用信号的组合器,从而使槽并发地执行。
在组合器调用期间,将执行以下步骤,以便在遍历信号的槽列表时查找下一个可调用槽。
- 锁住与槽连接相关联的互斥量。
- 所有跟踪到的与槽相关联的
weak_ptr
都被复制到临时shared_ptr
中,该临时shared_ptr
将一直保持活动状态,直到对槽的调用完成。如果由于任何一个weak_ptr
过期而导致此操作失败,则连接将自动断开。因此,如果某个槽所跟踪的任何weak_ptr
已经过期,那么该槽将永远不会运行,并且在槽运行时,该槽所跟踪的任何weak_ptr
都不会过期。 - 检查槽的连接是否被阻塞或断开,然后解锁连接的互斥量。如果连接被阻塞或断开,将从槽列表中的下一个槽重新开始。否则,我们将执行槽,当组合器调用迭代器解引用下一个槽时(除非组合器递增迭代器而不取消引用)。
注意,如果断开连接和信号是并发调用的,由于在执行关联的槽之前解锁了连接的互斥量,所以有可能槽在被connection::disconnect()
断开后仍然在执行。
你可能已经注意到,在信号调用过程中,调用只获得信号槽列表句柄和组合器的句柄,同时持有信号的互斥锁。因此,并发信号调用可能仍然会并发地访问相同的槽列表和组合器。那么,如果槽列表被修改了,例如通过连接一个新的槽,而信号调用同时进行,会发生什么呢?如果槽列表已经在使用中,信号在修改槽列表之前执行深拷贝。因此,并发信号调用将继续使用旧的未修改的槽列表,不受对槽列表新创建的深拷贝所做修改的干扰。新的信号调用将接收到新创建的深拷贝的槽列表句柄,旧的槽列表一旦不再使用就会被销毁。同样地,如果在一个信号调用并发运行时,用signal::set_combiner()
改变一个信号的组合器,则并发信号调用将继续不受干扰地使用旧的组合器,而新的信号调用将收到一个新组合器的句柄。
由于并发信号调用使用相同的组合器对象,这意味着需要确保所编写的任何自定义组合器都是线程安全的。因此,如果您的组合器维护的状态在组合器被调用时被修改,就可能需要使用互斥锁来保护该状态。要注意的是,如果在组合器中有一个互斥锁,而调用迭代器解引用槽时,如果任意的槽导致额外的互斥锁发生,就会可能有死锁和递归锁的风险。避免这些危险的一种方法是组合器在调用迭代器解引用槽之前释放所有锁。Boost.Signals2库提供的组合器类时线程安全的,因为它们调用时不需要维护任何状态。
假设写了一个槽,该槽将另一个槽连接到调用信号。新连接的槽是否会在创建新连接的相同信号调用期间运行?答案是否定的。连接新槽会修改信号的槽列表,如前所述,正在进行的信号调用不会看到对槽列表的任何修改。
假设写了一个槽,它断开了另一个槽与调用信号的连接。如果断开连接的槽出现在槽列表中的时间比断开连接的槽的时间晚,是否会阻止该槽在同一信号调用期间运行?这一次,答案是肯定的。即使断开连接的槽仍然存在于信号的槽列表中,每个槽都会被检查,看它是否在执行之前立即被断开或阻塞(或者不执行,视情况而定),正如上面所描述的那样。
2、连接和其他类
类signals2::connection
的方法是线程安全的,除了赋值和交换之外。这是通过锁定与对象的底层信号槽连接相关联的互斥锁来实现的。分配和交换不是线程安全的,因为互斥锁保护的是signals2::connection
对象引用的底层连接,而不是signals2::connection
对象本身。也就是说,signals2::connection
对象可能有多个拷贝,所有这些拷贝都引用相同的底层连接。每个signals2::connection
对象都没有互斥锁,只有一个互斥锁保护它们引用的底层连接。
类shared_connection_block
从保护阻塞和非阻塞的底层连接互斥锁中获得线程安全。用于跟中有多少shared_connection_block
对象在其底层连接上是断言块的内部引用计数也是线程安全的(该实现依赖于shared_ptr
进行引用计数)。但是,单个的shared_connection_block
对象不应该被多个线程并发地访问。只要两个线程都有自己的shared_connection_block
对象,那么线程就可以安全地使用它们,即使两个shared_connection_block
对象都是拷贝并引用相同的底层连接。
类signals2::slot
没有内建的内部互斥锁。它用于创建槽对象,然后在单线程中连接到信号。一旦它们被拷贝到信号的槽列表中,它们就会受到与每个信号槽连接相关联的互斥锁的保护。
类signals2::trackable
不提供线程安全的自动连接管理。特别是,如果可跟踪的派生对象在调用信号所在的不同线程中被销毁时,它将有可能使得信号调用到部分破坏的对象。signals2::trackable
只是为了方便移植Boost.Signals的单线程代码到Boost.Signals2。