Search code examples
pyside6

Use PySide6 in thread


Qt has a promising SCXML module. Since PySCXML is obsolete, there is no other native python scxml library, which lets me run a scxml statemachine. That's why I try PySide6.

Since I don't need any Qt despite of the scxml library, I thought about running the QCoreApplication in a seperate thread, in order to have the event-loop right there. According to the documentation QScxmlStateMachine needs one.

Unfortunately my start_statemachine() method doesn't return, but the statemachine starts working.

Any advice on how to start a QScxmlStateMachine in a thread is welcomed.

from PySide6.QtCore import QCoreApplication, QObject
from PySide6.QtScxml import QScxmlStateMachine
from PySide6.QtCore import QTimer
import threading


def start_statemachine(filename):
    app = QCoreApplication()
    mysm = MyStateMachine(filename)
    mysm.start_sm()
    app.exec()


class MyStateMachine(QObject):

    def __init__(self, filename):
        super(MyStateMachine, self).__init__()
        self.sm = QScxmlStateMachine.fromFile(filename)
        self.counter = 0
        self.timer = QTimer()
        self.timer.setInterval(2000)
        self.timer.timeout.connect(self.recurring_timer)
        self.timer.start()

    def start_sm(self):
        print('starting statemachine')
        self.sm.setRunning(True)

    def recurring_timer(self):
        print(self.sm.activeStateNames())
        self.counter += 1
        print("Counter: %d" % self.counter)
        print('statemachine running status: ' + str(self.sm.isRunning()))


if __name__ == '__main__':
    x = threading.Thread(target=start_statemachine('statemachine.scxml'))
    x.start() #won't be reached
    while True:
        pass #do something else
    x.join()

Solution

  • The thread target needs to be a reference to a function that will be called in the external thread, but you're not running start_statemachine() in another thread: you're actually executing it in place:

    x = threading.Thread(target=start_statemachine('statemachine.scxml'))
                                                  ^^^^^^^^^^^^^^^^^^^^^^
    

    Your program is stuck there, no thread is even created because the constructor is still "waiting" for start_statemachine() to return, and since exec() is blocking, nothing else happens.

    A basic solution could be to use a lambda:

    x = threading.Thread(target=lambda: start_statemachine('statemachine.scxml'))
    

    But you'll need access to the application in order to be able to quit it: x.join() won't do nothing, because the QCoreApplication event loop will keep going, so a possibility is to create a basic class that provides a reference to the application:

    class StateMachineWrapper:
        app = None
        def __init__(self, filename):
            self.filename = filename
    
        def start(self):
            self.app = QCoreApplication([])
            mysm = MyStateMachine(self.filename)
            mysm.start_sm()
            self.app.exec()
    
    # ...
    
    if __name__ == '__main__':
        statemachine = StateMachineWrapper('statemachine.scxml')
        x = threading.Thread(target=statemachine.start)
        x.start()
        while True:
            pass #do something else
    
        if statemachine.app:
            statemachine.app.quit()
    
        x.join()