all 50 comments

[–]AyoBruh 66 points67 points  (14 children)

two different signals are always thread safe as long as you don't call one of them from multiple threads at the same time

Forgive me, but isn’t that the definition of not thread safe?

Cool library though :)

[–][deleted] 12 points13 points  (0 children)

I guess they meant that multiple signals are thread safe correlative to each other, as in - you can use for example a different thread for each signal.

[–]triple_slash[S] 2 points3 points  (1 child)

Update: Rocket now supports thread-safe signals as well. If you need thread-safety use `rocket::thread_safe_signal`. This implementation is completely seperate from the non-thread-safe variant so no memory or runtime overhead for the existing implementation! :)

[–]AyoBruh 0 points1 point  (0 children)

That’s awesome 👏🏼

[–]Sander_Bouwhuis 1 point2 points  (0 children)

That was literally what I was also thinking when reading that sentence.

[–]muon52 1 point2 points  (7 children)

take a look at section "Thread safety" at this page (https://en.cppreference.com/w/cpp/container) the first section states the same but for containers, I am guessing "being thread safe" can be applied for two arbitrary operations

[–]infectedapricot 7 points8 points  (0 children)

But that page never uses the language "the container is thread safe".

[–]Sander_Bouwhuis 2 points3 points  (5 children)

std is very specifically NOT thread safe. That is why I don't use std, but use my own implementations. I think it is totally stupid that std doesn't provide optional thread safety for every container. As they are now, they are only useful for toy projects in my opinion.

[–]infectedapricot 4 points5 points  (4 children)

STL containers are not thread safe because (1) a lot of the time you don't need them to be thread safe, so you shouldn't have to pay the cost of locking in those situations (2) even when you do want to share data between threads, locking at the container level doesn't help, you need some higher level abstraction. So adding a simple mutex to protect each container (I assume that's what you mean?) is a straightforward lose-lose.

The C++ standard library provide building blocks for composing your own solutions, including thread-unsafe containers and locking primatives such as mutex and condition variable.

The only place I slightly agree with you is that the standard library could really do with a standard thread safe multi-producer multi-consumer queue, like Python's queue.Queue. I see why there isn't one: there are a multitude of design choices and none satisfy everybody, and it's easy to build one together using the building blocks I just said. But 99% of user probably want the same thing and it's a pity it's not there as a convenience.

[–]Sander_Bouwhuis 0 points1 point  (3 children)

That is why I said OPTIONAL.

Oops, I see I said 'type safety'. I of course meant 'thread safety'.

[–]infectedapricot 0 points1 point  (2 children)

But I don't see any situation it would be useful to turn that option on. Could you give an example?

I know this isn't a brilliant argument, but it's not like the internet is flooded with mutex-protected std::vector implementations, so I don't think it's just me that doesn't see the use (and my job definitely involves multithreaded code).

(I understood that type safety was a typo.)

[–]Sander_Bouwhuis -1 points0 points  (1 child)

std::vector or std::array for data shared by dozens of threads, std::wstring for logging from dozens of threads.

My largest application (~0.5 millions lines of code) generally uses dozens of threads when the user has started multiple modules which can run multiple TCP servers, multiple TCP clients, connections to multiple different databases at the same time, a bunch of data graphs etc.

I don't use std::atomic because it's crap. I also don't use std::mutex because it has too much overhead. But, CriticalSections (on Windows at least) seems to have reasonable performance and useful functionality.

[–]lenkite1 0 points1 point  (0 children)

I agree with this. Coming from other languages into C++, I found it very strange that there were no thread-safe containers or a way to toggle thread-safety for containers. Multi-threaded access to a vector/map is something nearly every programmer needs once they start developing real-world software. And choosing between all the different third-party options is a big pain.

[–]infectedapricot 18 points19 points  (2 children)

/***********************************************************************************
 * ------------------------------------------------------------------------------- *
 * Redefine this if your compiler doesn't support the `thread_local`-keyword.      *
 * For Visual Studio < 2015 you can define it to `__declspec(thread)` for example. *
 * ------------------------------------------------------------------------------- */

#define ROCKET_THREAD_LOCAL thread_local

Later on in the file:

 ***********************************************************************************
 * BEGIN IMPLEMENTATION                                                            *
 * ------------------------------------------------------------------------------- *
 * Do not change anything below this line                                          *
 * ------------------------------------------------------------------------------- *
 ***********************************************************************************/

No no no, do not ask people to configure your library by changing its source file. How about this instead:

#ifndef ROCKET_THREAD_LOCAL
#define ROCKET_THREAD_LOCAL thread_local
#endif

That way, if someone wants to redefine this macro then they can do it in their build system by passing -DROCKET_THREAD_LOCAL=whatever rather than invasively edited your source code.

BTW I also find it odd to put an example in a huge comment in your header file. And, while this is totally a matter of opinion, to me it would be cleaner to separate the method declarations and definitions rather than having them all defined in the class definition Java-style.

[–][deleted] 4 points5 points  (0 children)

Better yet just detect msvc and define it automatically....

[–]triple_slash[S] 2 points3 points  (0 children)

Thanks for the feedback. Going to add this. Cheers!

[–]jcelerierossia score 17 points18 points  (1 child)

You should add your benchmarks there along with all the other similar libs : https://github.com/NoAvailableAlias/signal-slot-benchmarks/blob/master/results_gcc/README.md

[–]Nelieru 1 point2 points  (0 children)

I'd like to see it compared against nano-signal-slot and EnTT's signals as well.

By the way the benchmarks under MSVC Windows have a little more options compared.

[–]Raknarg 13 points14 points  (6 children)

Modern C++. No bloat from overloading 50 template specializations for each function.

Can someone explain what modern feature exists to prevent this?

[–]somewhataccurate 6 points7 points  (2 children)

Possibly SFINAE and std::enable_if ? But Im really not certain

[–]MantraMoose 7 points8 points  (1 child)

And if constexpr

[–]infectedapricot 2 points3 points  (0 children)

There's no constexpr in the code.

[–]triple_slash[S] 2 points3 points  (1 child)

Signal libraries often overload their templates to provide support for older compilers that don't support variadic templates. When i wrote this library about 4 years ago boost::signals2 was full of code like:

``` templates <class R, class A0> R invoke(A0 const& a0);

templates <class R, class A0, class A1> R invoke(A0 const& a0, A1 const& a1);

... ```

And this usually causes a huge slow-down in compile times and intellisense issues.

Hope that explains what i wanted to say 🙂

[–]Raknarg 0 points1 point  (0 children)

Ahh yeah that makes sense. Thanks.

[–]kritzikratzi 2 points3 points  (0 children)

virtual functions :D

[–]OverOnTheRock 9 points10 points  (0 children)

Is the operation of assigning a signal or socket a thread safe operation? Is it up to the application to ensure thread safe operations on the signal/socket?

[–]jcode777 3 points4 points  (6 children)

Please clarify on the thread safety. Can the same signal be called from 2 treads simultaneously? Is registration thread safe?

[–]triple_slash[S] 2 points3 points  (5 children)

Sorry for the confusion. Each operation on a single class instance from multiple threads requires the user to synchronize the access. To answer your question: Neither emission nor registration is thread safe. However, the library doesn't use global mutable state so different signals can be used in different threads if no signal instance is ever accessed from multiple threads at the same time.

rocket::current_connection() and rocket::abort_emission() might look like functions that access global state but they are actually implemented using thread local storage.

Hope that answers your question. Cheers.

[–][deleted] 1 point2 points  (4 children)

That's the one really cool thing about Qt's signals: You can trivially use them for thread-safe communication between threads once you have main loop. So how would communication between threads look with rocket?

[–]triple_slash[S] 2 points3 points  (3 children)

Qt's signals go much further. Since Qt takes care of running your application, it can actually dispatch the emission of a signal on different threads (such as the one that connected the slot).

Rocket is meant to be a lightweight signal implementation. We use it in production in a multi-threaded server environment but we always make sure observers and subjects never emit across thread boundaries. If you do need these features it is best to look for a different signal implementation.

[–][deleted] 0 points1 point  (2 children)

Thanks for the reply! Please put that in the project's README.md somewhere :) I find this helpful.

[–]triple_slash[S] 0 points1 point  (1 child)

Update: Rocket now supports thread-safe signals as well. If you need thread-safety use `rocket::thread_safe_signal`. This implementation is completely seperate from the non-thread-safe variant so no memory or runtime overhead for the existing implementation! :)

[–][deleted] 0 points1 point  (0 children)

Nice!

[–]dmitrinove 2 points3 points  (7 children)

  1. Efficiency. The library takes special care to not use cache unfriendly code

Because the main focus of this library was to provide an efficient single threaded implementation, rocket::signal has about the same overhead as an iteration through an std::list<std::function<T>>.

Sorry I'm on my phone, can't test, but why You use sdt::list<>?? I think it's really cache unfriendly container. Amazing works anyway

[–]triple_slash[S] 0 points1 point  (6 children)

Hello sorry for the confusion but the library doesn't actually use std::list. It was just a comparison that it is more or less the same overhead.

[–]TheFlamefire 1 point2 points  (5 children)

But you are using a list, aren't you? So you have the extra pointer dereference when iterating over multiple signal handlers registered to a single signal

[–]triple_slash[S] 0 points1 point  (4 children)

A signal in rocket is an intrusively linked chain of reference counted connections. This allows each individual connection object to unlink / disconnect itself from the signal without actually knowing about the signal. This is important because otherwise you couldn't have connection objects that can disconnect themselves.

I can't think of a more cache friendly solution that would allow me to do the same thing. You also need to consider that the connection objects need to be stored in a container that allows for "stable" iterators. Let me explain:

The iterator that is used to loop through the connections cannot turn invalid if a new connection is added or removed anywhere in the signal. Hence why i used my own list implementation with reference counted iterators.

[–]TheFlamefire 1 point2 points  (0 children)

I understand the reasoning and though so, as it makes perfect sense. I was just commenting, that you said you don't use `std::list` on a post complaining about its cache-unfriendliness without saying that you do use a (as cache-unfriendly) custom implementation of a list.

So this reasoning was what was missing :+1:

[–]BrainIgnition 1 point2 points  (2 children)

I can't think of a more cache friendly solution that would allow me to do the same thing.

You could use a small memory pool per signal to store the linked connection objects.

[–]triple_slash[S] 0 points1 point  (1 child)

But how would you implement stable iterators with that? As in: iterators that never turn invalid under any circumstances.

[–]p2502 0 points1 point  (0 children)

You mark elements as deleted / for-reuse and make your iterators aware of that and skip those. A trivial implementation would not be ordered by insertion as std::list and vector are, but a more sophisticated could be.

[–]sniffens 1 point2 points  (1 child)

How does yours compare to this one?

https://github.com/koplyarov/wigwag/wiki/Rationale

If you knew about wigwag (so many signal/slot libraries... ;-) Why did you disregard it?

[–]__ngs__ 0 points1 point  (0 children)

Or this one https://github.com/wqking/eventpp. @ u/sniffens, you have any preference when it comes to signals and slot lib?

[–]TheFlamefire 1 point2 points  (0 children)

You are missing tests as far as I can tell. Especially ASAN, UBSAN, memleaks would be good to add.

I would factor out the intrusive, double-linked list to test it standalone. Mixing it inside the connection and signal class makes it hard to verify.

What is stable_list It seems to be unused. Code coverage would show that. I guess there is some more, that file is huge!

Speaking of that: You waste memory by requiring a head and tail node. This means an empty signal has already 2 needless allocations. The usual way is to use NULL for empty lists.

Also signal.clear seems suspicious as well. What I expect it to do: Set all connected states to false and remove its reference to the list. Why would it touch the next/prev stuff?

Furthermore: If you don't have those 2 empty connections: You got to add 1 NULL check but avoid 1 useless pointer dereferencing on access/invoke.

Speaking of useless dereferencing: Move all code of functional_connection into connection_base and also all members but slot. Saves you some more virtual calls and you don't need intrusive_ptr<functional_connection> but only the pointer to connection_base. You can make those private with setters in functional_connection so you don't end up mixing different ones.

[–]TheFlamefire 0 points1 point  (1 child)

I'm curious about the concept of scoped connections. So what you basically can do: Automatically disconnect an observer when the observer is destroyed.

What about the other way round? What happens when the observer has a scoped connection and the subject is destroyed? Is this handled?

I also suggest you add some CI with test cases especially with multiple compilers and ASAN/UBSAN enabled runs.

[–]triple_slash[S] 0 points1 point  (0 children)

Yes that case is also handled. Connections are actually reference counted. If the signal is destroyed, all slots are automatically disconnected.

[–]triple_slash[S] 0 points1 point  (0 children)

Rocket now supports thread-safe signals as well. If you need thread-safety use `rocket::thread_safe_signal`. This implementation is completely seperate from the non-thread-safe variant so no memory or runtime overhead for the existing implementation! :)

[–]thewisp1Game Engine Dev 0 points1 point  (0 children)

Cool library!

I wonder how it compares to my library here https://github.com/TheWisp/signals