3

I'm dealing with an application where many signals get fired after which a reconnect follows. I'll explain in detail how the application works, and also where my confusion starts.

 

1. Reconnecting a signal

In my application, I reconnect signals frequently. I will use the following static function for that, taken from the answer of @ekhumoro (and slightly modified) from this post: PyQt Widget connect() and disconnect()

def reconnect(signal, newhandler):
    while True:
        try:
            signal.disconnect()
        except TypeError:
            break
    if newhandler is not None:
        signal.connect(newhandler)

 

2. The application

Imagine the function emitterFunc(self) looping through a list of objects. Upon each iteration, the function connects mySignal to an object, fires the signal and then disconnects mySignal again at the start of the next iteration step. The fired signal also carries some payload, for example an object Foo().

 
enter image description here

EDIT:

  • The design shown above is simplified a lot. In the final design, the signal emitter and the receiving slot might operate in different threads.

  • For reasons that would lead us too far astray, I cannot just connect all the objects at once, emit a signal, and finally disconnect them all. I have to cycle through them one-by-one, doing a connect-emit-disconnect procedure.

  • Again for reasons that would lead us too far, I cannot just call these slots directly.

 

3. A mental image of the Signal-Slot mechanism

Over time, I have built up a mental image of how the Signal-Slot mechanism works. I imagine a Signal-Slot engine absorbing all fired signals and putting them in a Queue. Each signal awaits its turn. When time is ready, the engine passes the given signal to the appropriate handler. To do that correctly, the engine has some 'bookkeeping' work to ensure each signal ends up in the right slot.

enter image description here

 

4. Behavior of the Signal-Slot engine

Imagine we're at the nth iteration step. We connect self.mySignal to object_n. Then we fire the signal with its payload. Almost immediately after doing that, we break the connection and establish a new connection to object_n+1. At the moment we break the connection, the fired signal probably didn't do its job yet. I can imagine three possible behaviors of the Signal-Slot engine:

  • [OPTION 1] The engine notices that the connection is broken, and discards sig_n from its Queue.

  • [OPTION 2] The engine notices that the connection is re-established to another handler, and sends sig_n to the handler of object_n+1 (as soon as it gets to the front of the Queue).

  • [OPTION 3] The engine doesn't change anything for sig_n. When fired, it was intended for the handler of object_n, and that's where it will end up.

 

5. My questions

My first question is pretty obvious by now. What is the correct Signal-Slot engine behavior? I hope it is the third option.

As a second question, I'd like to know to what extent the given mental image is correct. For example, can I rely on the signals getting out of the Queue in order? This question is less important - it's certainly not vital to my application.

The third question has to do with time efficiency. Is reconnecting to another handler time-consuming? Once I know the answer to the first question, I will proceed building the application and I could measure the reconnection time myself. So this question is not so vital. But if you know the answer anyway, please share :-)

K.Mulier
  • 8,069
  • 15
  • 79
  • 141

1 Answers1

3

I would start with your second question, to say that your mental image is partially correct, because a queue is involved, but not always. When a signal is emitted, there are three possible ways of calling the connected slot(s) and two of them use the event queue (a QMetaCallEvent is instantiated on the fly and posted with QCoreApplication's method postEvent, where the event target is the slot holder, or the signal receiver, if you prefer). The third case is a direct call, so emitting the signal is just like calling the slot, and nothing get queued.

Now to the first question: in any case, when the signal is emitted, a list of connections (belonging to the signal emitter) is traversed and slots are called one after the other using one of the three methods hinted above. Whenever a connection is made, or broken, the list will be updated, but this necessarily happens before or after the signal is emitted. In short: there is very little chances to succeed in blocking a call to a connected slot after the signal has been emitted, at least not breaking the connection with disconnect(). So I would mark the [OPTION 3] as correct.

If you want to dig further, start from the ConnectionType enum documentation where the three fundamental types of connection (direct, queued and blocking-queued) are well explained. A connection type can be specified as a fifth argument to QObject's method connect, but, as you'll learn from the above linked docs, very often is Qt itself to choose the connection type that best suits the situation. Spoiler: threads are involved :)

About the third question: I have no benchmark tests at hand to show, so I will give a so called primarily opinion based answer, the kind of answer that starts with IMHO. I think that the signal/slot realm is one of those where the keep-it-simple rules do rule, and your reconnect pattern seems to make things much complicated than they need to be. As I hinted above, when a connection is made, a connection object is appended to a list. When the signal is emitted, all the connected slots will be called somehow, one after the other. So, instead of disconnect/reconnect/emit at each cycle in your loop, why don't just connect all items first, then emit the signal, then disconnect them all?

I hope my (long and maybe tldr) answer helped. Good read.

p-a-o-l-o
  • 9,807
  • 2
  • 22
  • 35
  • Hi @p-a-o-l-o , thank you very much, your answer is very helpful!. I have a couple of questions --- (1) You say that there are three possible ways to call the connected slot(s). I can only read two in your answer: the one with a `QMetaCallEvent` and the direct one. What is the third one? --- (2) You say that there is **very little chances** things go wrong, so you mark my `[OPTION 3]` as correct. When you say **very little chances**, I feel a bit reluctant to rely on it. What do you think? – K.Mulier May 22 '18 at 15:35
  • --- (3) The application in my question is very much simplified. In that simplified version, one could indeed connect all items first, then emit the signal, then disconnect them all. But believe me, in the *real* application, this is not an option. For reasons that would lead us too far astray, I need to disconnect/reconnect/emit at each cycle. – K.Mulier May 22 '18 at 15:37
  • (1) A `QMetaCallEvent` is posted in case of queued connection or blocking-queued connection (where the signalling thread blocks until the slot returns). (2) There are few chances to prevent the slot to be called after the signal has been emitted, and no chances at all to do it using `disconnect`. – p-a-o-l-o May 22 '18 at 15:40
  • (3) Obviously, I imagine you didn't choose that design by chance. I just wanted to be sure you understood that *connecting* means *adding to a list of connections*. Anyway, if your application is single-threaded or threading isn't involved in your list of calls, I would resort to a traditional pattern, i.e. call the object slots (methods) directly and drop connections altogether. Again, I can only guess. Anyway, I'm glad if I contributed somehow. – p-a-o-l-o May 22 '18 at 15:47
  • Thank you sir. You surely contributed more than you imagine :-) . I've got just one more question. You say *"there are few chances to prevent the slot to be called after the signal has been emitted, and certainly no chances at all to do it using `disconnect`."*. I'm happy to hear that. But what about `reconnect`? I mean, is there a chance that the *wrong* slot gets called (because while sitting in the Queue, the signal got connected to another slot). To quote the example in my question above, could `sig_n` end up with the handler of **object_n+1**? I just want to be sure this can't happen ;-) – K.Mulier May 22 '18 at 15:57
  • If it's a direct connection, the slot will be called before the disconnect. If it's a queued one, the event posted will eventually reach its receiver, and it will be the right one, no way to change the event target by calling disconnect, they're just two worlds apart. So, 100% sure :) – p-a-o-l-o May 22 '18 at 16:05
  • @K.Mulier. There is no queue involved at all, unless the signals are emitted across threads. In your example, the slots will be called entirely synchronously and in a strictly serial fashion according to the order in which they were connected. In other words, every step of the code execution *blocks*, and there is no chance of any concurrency whatsoever. Your current design is in fact redundant. The most efficient solution is to simply call the slots directly, and not use signals at all. – ekhumoro May 22 '18 at 16:09
  • Hi @ekhumoro , thank you very much for your help. I just added an **EDIT** to clarify my intentions. This EDIT also explains why my current example looks a bit awkward (as it is overly simplified in an attempt to keep the focus on the signal-slot-mechanism). I'm sorry I didn't clarify it earlier. – K.Mulier May 22 '18 at 16:27