2

When a QObject is created in a different thread and moved back to the main thread using QObject.moveToThread, lambda signals "disconnect" (they won't fire). My guess is that regular slots are linked to the QObject that is moved to the main thread, so they run in the main thread's event loop, but lambda functions aren't linked to the QObject, so there is no event loop for them to run.

This can be seen in the following short Python code:

if __name__ == "__main__":
    from traits.etsconfig.api import ETSConfig
    ETSConfig.toolkit = 'qt4'

import threading
from PySide import QtGui, QtCore

class MyObject(QtCore.QObject):
    def __init__(self, button):
        super(MyObject, self).__init__()
        button.clicked.connect(self.mySlot)
        button.clicked.connect(lambda: self.mySlot('lambda'))
    #
    def mySlot(self, printing='object'):
        print printing
#
myObj = None
# global variable to keep it in memory
def myThread(mainThread, button):
    global myObj
    myObj = MyObject(button)
    myObj.moveToThread(mainThread)
#
if __name__ == '__main__':
    appQT = QtGui.QApplication([])
    #
    myWidget = QtGui.QWidget()
    myButton = QtGui.QPushButton('Press to see output')
    myLayout = QtGui.QVBoxLayout(myWidget)
    myLayout.addWidget(myButton)
    myWidget.show()
    #
    mainThread = QtCore.QThread.currentThread()
    if True:
        # run myThread in a new thread
        # prints only the first slot (object slot)
        threading.Thread(target=myThread, args=[mainThread, myButton]).start()
    else:
        # run myThread in this thread
        # prints both slots (object and lambda slots)
        myThread(mainThread, myButton)
    #
    appQT.exec_()

You can see how the results differ from expected by changing the conditional from True to False.

When it is set to True, the output after clicking the button is:

object

When it is set to False, the output after clicking the button is:

object
lambda

My question is, can someone explain more precisely why it behaves in this way, and is there an easy way to keep the lambda slots working when moving the QObject back to the main thread?

Steve
  • 542
  • 1
  • 4
  • 12
  • Try replacing `lambda: self.mySlot('lambda')` with `lambda self=self: self.mySlot('lambda')`; I think you are experiencing a scope issue. – chepner Jan 21 '16 at 19:41
  • Thanks chepner, I just tried it but it gives the same results, so that does not seem to be the issue. – Steve Jan 21 '16 at 19:44
  • Never used Python in Qt or even Python itself too much, but don't you think that lambda connections are implemented with some hidden QObject? Try to add an event loop into that thread, maybe it'll appear. Also, you can explore it with GDB, Qt C++ code is very debuggable, especially if you're on something like Debian. – Velkan Jan 22 '16 at 07:40
  • Thanks for the comment Velkan, that would be a good next step, to change the Python threads into QThreads and see what happens. I'll try this when I have a chance. – Steve Jan 22 '16 at 16:58

1 Answers1

1

Okay, I figured out some details about what is going on. It's still a partial answer, but I think it's more suitable as an answer rather than update to the question.

It seems that I was correct in my original question that the slots are linked to their instance object's QObject event loop (thread), but only if that slot is a bound method (has an object instance).

If you look into the PySide source code on Github, you'll see that it defines the receiver (the QObject that receives the signal) based on the type of slot it receives. So if you pass the QtSignal.connect() function a bound object method, the receiver is defined as the slotMethod.__self__ (which is PyMethod_GET_SELF(callback)). If you pass it a general callable object (for example a lambda function) which is not bound (no __self__ property), the receiver is simply set to NULL. The receiver tells Qt which event loop to send the signal to, so if it's NULL, it doesn't know to send the signal to the main event loop.

Here's a snippet of the PySide source code:

static bool getReceiver(QObject *source, const char* signal, PyObject* callback, QObject** receiver, PyObject** self, QByteArray* callbackSig)
{
    bool forceGlobalReceiver = false;
    if (PyMethod_Check(callback)) {
        *self = PyMethod_GET_SELF(callback);
        if (%CHECKTYPE[QObject*](*self))
            *receiver = %CONVERTTOCPP[QObject*](*self);
        forceGlobalReceiver = isDecorator(callback, *self);
    } else if (PyCFunction_Check(callback)) {
        *self = PyCFunction_GET_SELF(callback);
        if (*self && %CHECKTYPE[QObject*](*self))
            *receiver = %CONVERTTOCPP[QObject*](*self);
    } else if (PyCallable_Check(callback)) {
        // Ok, just a callable object
        *receiver = 0;
        *self = 0;
    }

    ...
    ...
}

Does this help us fix our problem with the lambda functions? Not really... If we bind the lambda functions using the following (with types.MethodType), the behavior does not change:

import types

class MyObject(QtCore.QObject):
    def __init__(self, button):
        super(MyObject, self).__init__()
        button.clicked.connect(self.mySlot)
        thisLambda = lambda self=self : self.mySlot('hello')
        self.myLambda = types.MethodType( thisLambda, self )
        button.clicked.connect(self.myLambda)
    #
    def mySlot(self, printing='object'):
        print printing

Output:

object

This binding is definitely part of the problem since I have demonstrated below that the same behavior occurs with non-bound global methods, and by binding them using types.MethodType(), it fixes the problem:

import types

def abc(self):
    print 'global1'

def xyz(self):
    print 'global2'

class MyObject(QtCore.QObject):
    def __init__(self, button):
        super(MyObject, self).__init__()
        button.clicked.connect(self.mySlot)
        self.xyz = types.MethodType( xyz, self )
        button.clicked.connect(abc)
        button.clicked.connect(self.xyz)
    #
    def mySlot(self, printing='object'):
        print printing

Output:

object
global2

Anyways, it seems the simplest solution is just to not create the QObject in a separate thread in the first place, but this answer is a step towards understanding why it doesn't work properly.

Community
  • 1
  • 1
Steve
  • 542
  • 1
  • 4
  • 12