Skip to content

Signals/Slots implementation based off of atomic operations, with fast emits.

Notifications You must be signed in to change notification settings

helloworld922/signals3

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

58 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

signals3 README

(c) 2013 helloworld922

Distributed under the Boost Software License, Version 1.0. (See
accompanying file LICENSE_1_0.txt or copy at
http://www.boost.org/LICENSE_1_0.txt)
=======================================================================

Signals/Slots implementation based off of atomic operations, with lock-free* emits.

The target is to implement all features currently offered by Boost Signals2 while keeping cheap* emits.

Currently implemented towards the C++11 standard, but the goal is to eventually back-port to support C++03 using other Boost libraries.

*note: I thought Atomic shared_ptr operations in both Boost and the Standard Library might have a lock-free shared_ptr, but I realized this was a lie.
	They use spinlocks underneath to achieve atomicity. Never-the-less, this library does perform quite well in many cases, but not all cases.
	I may look deeper into providing a shared_ptr/weak_ptr like implementation which really is lock-free/faster in the future, but I don't have any plans for now to do so.

Current dependencies:

- c++11 Variadic Templates
- c++11 move semantics and lvalue refs
- boost::move*
- c++11 forward_list
- c++11 std::forward
- c++11 std::tuple (boost::tuple doesn't work do to problems unpacking)
- boost optional

note*: boost::move internally will bind to std::move if compiler support is available.

In addition to these hard requirements, the library will substitute Boost libraries in if the appropriate standard library isn't available.

- boost::mutex			-or-	c++11 std::mutex
- boost::unique_lock	-or-	c++11 std::unique_lock
- boost::atomic			-or-	c++11 std::atomic
- boost::shared_ptr		-or-	c++11 std::shared_ptr**
- boost::weak_ptr		-or-	c++11 std::weak_ptr**
- boost::make_shared	-or-	c++11 std::make_shared**
- boost::function***	-or-	c++11 std::function

note**: If there is no c++11 atomic shared_ptr, the library will only use boost::shared_ptr/boost::weak_ptr.
note***: boost::function*** doesn't seem to forward parameters correctly, only copies (multiple times potentially).

hopefully that's all of them.


======================
General Use
======================
The library is a header-only library unless boost::thread is used, which relies on boost::system which does have a compiled library.
See the boost documentation for how to build boost libraries.

Header file description:
	boost/signals3/signals3.hpp - main header file which gathers all header files
	boost/signals3/signal.hpp - definition for signal class
	boost/signals3/optional_last_value.hpp - optional_last_value combiner (basically the same as Signals2)
	boost/signals3/slots.hpp - definition for slot
	boost/signals3/connection.hpp - connection header file

For the most part including signals3/signals3.hpp will work. There are many interface naming differences (should function the same, though):

- signal.operator() is now signal.emit
- signal.connect has been split into signal.push_front/signal.push_back
- connecting grouped slots is done using signal.insert and associated variants
- you can disconnect the first/last slot using signal.pop_front/signal.pop_back
- To disconnect all slots use signal.clear
- To disconnect a certain group of slots use signal.erase
- There are explicit thread unsafe handles of various functions. These have the same name as the thread-safe version, with an _unsafe at the end. For example: signal.emit_unsafe
	Not all functions have an _unsafe version. Also, keep in mind that shared_ptr/weak_ptr may use certain thread-safety features even if they aren't used in a thread-safe manner. 

For the most part nearly all of the features of Boost Signals2 have been implemented, but I have not done extensive testing yet so there may be many fatal bugs.

-----------------------------------
Rational for interface name changes
-----------------------------------
Connect/disconnect names were changed to more closely reflect the terminology used by Standard Library containers.
Emit operation was changed from operator() to emit so that signals could be emitted with thread-safety traits using emit or emit_unsafe.

Notable missing features:

- disconnect specific slot by passing a function/slot object.
- no shared connection block (underlying details are implemented, just public handlers aren't available). It's possible to block/unblock any connection, though this might be changed in the future (I actually think this isn't possible in Signals2).
- connection/scoped_connection basically work, but may not have all the implementation details/work exactly like they do in Boost Signals2
- possibly other features
- Trackable. This was a backwards-compatibility feature from Signals2 to Signals. I don't plan on implementing this ever (use the tracking scheme of Signals2 instead).

All handles reside in the boost::signals3 namespace.

====================
Design Goals
====================
My primary motivation for writing this library was looking at how Signals/Slots are typically used.
Namely:

1. Connecting/disconnecting slots is usually not that time critical.
2. Most signals either have no slots, or very few slots connected.
3. Signals may be invoked from multiple threads, and usually can be evaluated asynchronously. It seems like slots usually can be evaluated asynchronously, but they could be strongly ordered, too. In any case, only forward iteration is required for emitting a signal.
4. Emitting usually happens significantly more than modifying a signal, and sometimes in time-critical event loops.

Based on these requirements I decided the main focus would be on implementing Signals/Slots with a highly optimized emit implementation.
This was done using a doubly-linked list which is always atomically a consistent singly linked list.
This allows for cheap* signal emit and a single writer (write operations typically must obtain a unique lock).
Additionally, handlers have been added which allow for thread-unsafe operations which allow for external synchronization, or no synchronization at all if the signal will not be modified under general use.

*note: see note in the introduction about my lock-free assumptions, which turned out to be false. The library still performs quite well in many cases (compared to Boost Signals2), but not all cases.

The library is fully re-entrant, thread-safe* (with thread-unsafe handlers added), and type-safe.

*note: thread safety guarantee is similar to Boost Signals2 thread safety. For example, building a slot is not implemented in a thread-safe manner.
Another observation about thread safety is that it is not safe to implement multi-threaded combiners: a combiner must run exclusively on the thread it was called from.

Further more, on return the iterators passed in are invalid.

====================
Issues/"Design Features"
====================

Two issue/design feature I noticed was in forwarding of parameters and use of rvalue references.

--------
Forwarding Parameters
--------

class my_class;
void slot(my_class);

signal<void(my_class)> msig;

msig.push_back(slot); // slot1
msig.push_back(slot); // slot2

msig.emit(my_class()); // on calling each slot, temporary rvalue is always copied and passed to each slot

This code will always pass a copy of the parameters to each slot.
This is done to prevent perfect forwarding moving the internals of the original my_class object into the parameter for slot1, then calling slot2 with the gutted original rvalue.

As a consequence it is impossible to perfectly forward parameters to a slot:

void slot(std::vector<int> vals);

signal<void(std::vector<int>)> msig;
msig.push_back(slot);
std::vector<int> x = {1, 2, 3, 4, 5};

msig.emit(std::move(x)); // copies vector x to each slot
// observable: contents of x are unchanged

This is seen as a minor issue because in theory this type of code is only safe/desirable if there is exactly zero or one slot.
I would recommend using lvalue reference parameters as has been done in the past.

void slot(std::vector<int>& vals);

signal<void(std::vector<int>)> msig;
msig.push_back(slot);
std::vector<int> x = {1, 2, 3, 4, 5};

msig.emit(std::move(x)); // copies vector x to each slot
// observable: contents of x are unchanged

--------
Rvalue reference parameters on slots
--------

As a result of the above it is impossible to emit slots which take rvalue reference parameter types.

void slot(my_class&& arg);

signal<void(my_class&&)> msig; // valid
msig.push_back(slot); // valid

msig.emit(my_class()); // problem!

This is a slightly different case of the above where the internals of a parameter could be moved away in a slot, leaving it invalid for all remaining slots.
It isn't immediately obvious why this would ever be desirable, and a suitable fix isn't obvious to me.
Simply avoid doing this; it may even be beneficial to add template checks to actively prevent declaring signals which could have rvalue reference parameters.

About

Signals/Slots implementation based off of atomic operations, with fast emits.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages