note: This is v2 of code that was previously reviewed.
I have written a class that handles listeners registering callbacks to receive messages. Link lifetime is managed (or is it?). Code is multithreaded in that any thread can broadcast to the listeners, and any thread can add or remove listeners.
I have two concerns:
I am not sure if this method for lifetime management works, or if there is a race condition. Is the following a possible race condition?
- broadcaster obtains shared pointer;
- before it can invoke the callback, the listener destructs (imaging its a ref to a class member function);
- Listener is invoked, but points to an object that no longer exists.
It is possible that the broadcaster is destructed while another thread has invoked one of the member functions, and thus holds (or is trying to acquire) the mutex. That would crash?
Implementation
#include <vector>
#include <mutex>
#include <memory>
#include <functional>
#include <type_traits>
#include <numeric>
// based on https://stackoverflow.com/a/47872677
// usage notes:
// 1. It is safe to call any of the member functions
// from inside a listener invoked by this class.
// 2. Listeners added from inside listener callback
// will participate starting from the next message.
// 3. This class guarantees message ordering with
// multiple producer threads.
template <class... Message>
class Broadcaster
{
public:
using listener = std::function<void(Message...)>;
using cookieType = std::shared_ptr<void>;
// returns number of registered listeners
size_t num_listeners() const noexcept
{
auto l = std::unique_lock(_mut);
return std::accumulate(_targets.cbegin(), _targets.cend(), size_t{ 0 }, [](size_t cur, const auto& m) { return cur + !m.expired(); });
}
// clears all listeners
void clear() noexcept
{
auto l = std::unique_lock(_mut);
_targets.clear();
}
template <class F> requires (std::is_nothrow_invocable_r_v<void, F, Message...>)
[[nodiscard]] cookieType add_listener(F&& r_)
{
auto l = std::unique_lock(_mut);
auto listenFunc = std::make_shared<listener>(std::forward<F>(r_));
_targets.push_back(listenFunc);
return listenFunc;
}
void notify_all(const Message&... msg_) noexcept
{
auto lt = std::unique_lock(_mut);
auto ls = std::unique_lock(_sendMut);
// remove dead listeners
_targets.erase(
std::remove_if(_targets.begin(), _targets.end(),
[](const auto& ptr) { return ptr.expired(); }
),
_targets.end()
);
// take copy
auto tmp = _targets;
lt.unlock();
// 1. mutex unlocked, so new listeners can be added
// 2. sending mutex remains held so that message
// ordering is guaranteed.
// invoke all listeners with message
for (auto&& f : tmp)
{
if (auto targetPtr = f.lock(); targetPtr)
targetPtr->operator()(msg_...);
}
}
private:
mutable std::mutex _mut;
std::mutex _sendMut;
std::vector<std::weak_ptr<listener>> _targets;
};
Example usage
#include <iostream>
#include <string>
#include <functional>
void freeTestFunction(std::string msg_) noexcept
{
std::cout << "from freeTestFunction: " << msg_ << std::endl;
}
struct test
{
using StringBroadcaster = Broadcaster<std::string>;
void simpleCallback(std::string msg_) noexcept
{
std::cout << "from simpleCallback: " << msg_ << std::endl;
}
void oneShotCallback(std::string msg_) noexcept
{
std::cout << "from oneShotCallback: " << msg_ << std::endl;
_cb_oneShot_cookie.reset();
}
void twoStepCallback_step1(std::string msg_) noexcept
{
std::cout << "from twoStepCallback_step1: " << msg_ << std::endl;
// replace callback
_cb_twostep_cookie = _broadcast.add_listener([&](auto fr_) noexcept { twoStepCallback_step2(fr_); });
}
void twoStepCallback_step2(std::string msg_) noexcept
{
std::cout << "from twoStepCallback_step2: " << msg_ << std::endl;
}
void runExample()
{
auto cb_simple_cookie = _broadcast.add_listener([&](auto fr_) noexcept { simpleCallback(fr_); });
_cb_oneShot_cookie = _broadcast.add_listener([&](auto fr_) noexcept { oneShotCallback(fr_); });
_cb_twostep_cookie = _broadcast.add_listener([&](auto fr_) noexcept { twoStepCallback_step1(fr_); });
auto free_func_cookie = _broadcast.add_listener(&freeTestFunction);
_broadcast.notify_all("message 1"); // should be received by simpleCallback, oneShotCallback, twoStepCallback_step1, freeTestFunction
free_func_cookie.reset();
_broadcast.notify_all("message 2"); // should be received by simpleCallback and twoStepCallback_step2
cb_simple_cookie.reset();
_broadcast.notify_all("message 3"); // should be received by twoStepCallback_step2
_cb_twostep_cookie.reset();
_broadcast.notify_all("message 4"); // should be received by none
}
StringBroadcaster _broadcast;
StringBroadcaster::cookieType _cb_oneShot_cookie;
StringBroadcaster::cookieType _cb_twostep_cookie;
};
int main(int argc, char **argv)
{
test t;
t.runExample();
return 0;
}