Le piège des QThread

Article original publié chez Makina Corpus

Il y a de nombreux billets de blogs, posts sur des forums, tutoriaux, pages Wiki et autres, mais au final, à part le fameux "You're doing it wrong", qui peut paraître obscure au premier abord, je n'ai pas trouvé de résumé de l'attrape-nigaud que je vais illustrer ici.

Le piège

Naturellement, quand on veut faire une thread, on a envie d'hériter de l'objet QThread. C'est ce qu'on fait avec le module threading de python (en Java aussi il me semble).

Voici ce qu'on écrit naturellement : Objet, la classe qui file l'ordre et Worker, une classe qui bosse dur en arrière plan. On connecte les signaux et on démarre !

import sys
from PyQt4.QtCore import *
from PyQt4.QtGui  import QApplication

class Object(QObject):
    def emitSignal(self):
        self.emit(SIGNAL("aSignal()"))

class Worker(QThread):
    def aSlot(self):
        self.thread().sleep(1)
        print "Slot is executed in thread : ", self.thread().currentThreadId()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    print "Main application thread is : ", app.thread().currentThreadId()

    worker = Worker()
    obj = Object()
    QObject.connect(obj, SIGNAL("aSignal()"), worker.aSlot)

    worker.start()
    obj.emitSignal()

    print "Done."
    app.exec_()

Ici, comme le slot aSlot() est défini dans la classe Worker, qui hérite de QThread, on pense naturellement qu'il va être exécuté en arrière-plan. Que nenni!

Main application thread is :  140068661352224
# (... wait 1 sec ...)
Slot is executed in thread :  140068661352224
Done.

La solution

Un secret ? Les QThread ne sont pas des threads. Elles enrobent l'execution d'une thread.

L'appartenance (affinité) d'un objet à une thread détermine le type de connexion utilisé par défaut, et par conséquent le comportement lors de l'execution des slots.

Ce qu'il faut écrire : Worker n'est plus une QThread, on force son affinité dans une thread avec moveToThread().

class Object(QObject):
    def emitSignal(self):
        self.emit(SIGNAL("aSignal()"))

class Worker(QObject):
    def aSlot(self):
        self.thread().sleep(1)
        print "Slot is executed in thread : ", self.thread().currentThreadId()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    print "Main application thread is : ", app.thread().currentThreadId()

    worker = Worker()
    obj = Object()

    thread = QThread()
    worker.moveToThread(thread)
    QObject.connect(obj, SIGNAL("aSignal()"), worker.aSlot)

    thread.start()
    obj.emitSignal()

    print "Done."
    app.exec_()

Désormais, l'exécution est bien asynchrone, comme on le souhaitait.

Main application thread is :  139961882056480
Done.
# (... wait 1 sec ...)
Slot is executed in thread :  139961512900352

Tout simplement ! Si j'avais lu mon article avant, je n'aurais pas perdu autant de temps à lire toutes ces docs ambiguës sur le Net.

Sources:

#qt, #python, #tips - Posted in the Dev category