Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

PrevUpHomeNext

Thread-Safety

Introduction
Signals and combiners
Connections and other classes

Introduction

The primary motivation for Boost.Signals2 is to provide a version of the original Boost.Signals library which can be used safely in a multi-threaded environment. This is achieved primarily through two changes from the original Boost.Signals API. One is the introduction of a new automatic connection management scheme relying on shared_ptr and weak_ptr, as described in the tutorial. The second change was the introduction of a Mutex template type parameter to the signal class. This section details how the library employs these changes to provide thread-safety, and the limits of the provided thread-safety.

Signals and combiners

Each signal object default-constructs a Mutex object to protect its internal state. Furthermore, a Mutex is created each time a new slot is connected to the signal, to protect the associated signal-slot connection.

A signal's mutex is automatically locked whenever any of the signal's methods are called. The mutex is usually held until the method completes, however there is one major exception to this rule. When a signal is invoked by calling signal::operator(), the invocation first acquires a lock on the signal's mutex. Then it obtains a handle to the signal's slot list and combiner. Next it releases the signal's mutex, before invoking the combiner to iterate through the slot list. Thus no mutexes are held by the signal while a slot is executing. This design choice makes it impossible for user code running in a slot to deadlock against any of the mutexes used internally by the Boost.Signals2 library. It also prevents slots from accidentally causing recursive locking attempts on any of the library's internal mutexes. Therefore, if you invoke a signal concurrently from multiple threads, it is possible for the signal's combiner to be invoked concurrently and thus the slots to execute concurrently.

During a combiner invocation, the following steps are performed in order to find the next callable slot while iterating through the signal's slot list.

  • The Mutex associated with the connection to the slot is locked.

  • All the tracked weak_ptr associated with the slot are copied into temporary shared_ptr which will be kept alive until the invocation is done with the slot. If this fails due to any of the weak_ptr being expired, the connection is automatically disconnected. Therefore a slot will never be run if any of its tracked weak_ptr have expired, and none of its tracked weak_ptr will expire while the slot is running.

  • The slot's connection is checked to see if it is blocked or disconnected, and then the connection's mutex is unlocked. If the connection was either blocked or disconnected, we start again from the beginning with the next slot in the slot list. Otherwise, we commit to executing the slot when the combiner next dereferences the slot call iterator (unless the combiner should increment the iterator without ever dereferencing it).

Note that since we unlock the connection's mutex before executing its associated slot, it is possible a slot will still be executing after it has been disconnected by a connection::disconnect(), if the disconnect was called concurrently with signal invocation.

You may have noticed above that during signal invocation, the invocation only obtains handles to the signal's slot list and combiner while holding the signal's mutex. Thus concurrent signal invocations may still wind up accessing the same slot list and combiner concurrently. So what happens if the slot list is modified, for example by connecting a new slot, while a signal invocation is in progress concurrently? If the slot list is already in use, the signal performs a deep copy of the slot list before modifying it. Thus the a concurrent signal invocation will continue to use the old unmodified slot list, undisturbed by modifications made to the newly created deep copy of the slot list. Future signal invocations will receive a handle to the newly created deep copy of the slot list, and the old slot list will be destroyed once it is no longer in use. Similarly, if you change a signal's combiner with signal::set_combiner while a signal invocation is running concurrently, the concurrent signal invocation will continue to use the old combiner undisturbed, while future signal invocations will receive a handle to the new combiner.

The fact that concurrent signal invocations use the same combiner object means you need to insure any custom combiner you write is thread-safe. So if your combiner maintains state which is modified when the combiner is invoked, you may need to protect that state with a mutex. Be aware, if you hold a mutex in your combiner while dereferencing slot call iterators, you run the risk of deadlocks and recursive locking if any of the slots cause additional mutex locking to occur. One way to avoid these perils is for your combiner to release any locks before dereferencing a slot call iterator. The combiner classes provided by the Boost.Signals2 library are all thread-safe, since they do not maintain any state across invocations.

Suppose a user writes a slot which connects another slot to the invoking signal. Will the newly connected slot be run during the same signal invocation in which the new connection was made? The answer is no. Connecting a new slot modifies the signal's slot list, and as explained above, a signal invocation already in progress will not see any modifications made to the slot list.

Suppose a user writes a slot which disconnects another slot from the invoking signal. Will the disconnected slot be prevented from running during the same signal invocation, if it appears later in the slot list than the slot which disconnected it? This time the answer is yes. Even if the disconnected slot is still present in the signal's slot list, each slot is checked to see if it is disconnected or blocked immediately before it is executed (or not executed as the case may be), as was described in more detail above.

Connections and other classes

The methods of the signals2::connection class are thread-safe, with the exception of assignment and swap. This is achived via locking the mutex associated with the object's underlying signal-slot connection. Assignment and swap are not thread-safe because the mutex protects the underlying connection which a signals2::connection object references, not the signals2::connection object itself. That is, there may be many copies of a signals2::connection object, all of which reference the same underlying connection. There is not a mutex for each signals2::connection object, there is only a single mutex protecting the underlying connection they reference.

The shared_connection_block class obtains some thread-safety from the Mutex protecting the underlying connection which is blocked and unblocked. The internal reference counting which is used to keep track of how many shared_connection_block objects are asserting blocks on their underlying connection is also thread-safe (the implementation relies on shared_ptr for the reference counting). However, individual shared_connection_block objects should not be accessed concurrently by multiple threads. As long as two threads each have their own shared_connection_block object, then they may use them in safety, even if both shared_connection_block objects are copies and refer to the same underlying connection.

The signals2::slot class has no internal mutex locking built into it. It is expected that slot objects will be created then connected to a signal in a single thread. Once they have been copied into a signal's slot list, they are protected by the mutex associated with each signal-slot connection.

The signals2::trackable class does NOT provide thread-safe automatic connection management. In particular, it leaves open the possibility of a signal invocation calling into a partially destructed object if the trackable-derived object is destroyed in a different thread from the one invoking the signal. signals2::trackable is only provided as a convenience for porting single-threaded code from Boost.Signals to Boost.Signals2.


PrevUpHomeNext