Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
325 views
in Technique[技术] by (71.8m points)

python - Syncing activity in PyQt QThreads

I'm playing around with PyQt, and QThreads. It seems that if I use the code that I've put in this python fiddle (note that the top section is auto-generated code from QtDesigner), where the current value of the loop is printed both in the loop in the slave thread and the loop controlling the progress bar, then the loops keep in sync, the values match at all points as the program runs, and the progress bar displays accurately the proportion of the slave thread completed.

In response to a comment below, this program in its current state actually does what I want it to - it just prints out, to the terminal, the value of the loop in the slave thread and the the value in the loop which controls the progression of the progress bar.

However, commenting out line 121 (i.e. if you don't print the current value in the progress bar loop), results in the progress bar reaching 100% (i.e. finishing 300 iterations) when the slave thread loop has only reached ~130 iterations (i.e. the progress bar completes about 100% faster).

Have I done something na?vely stupid / wrong - is there a better way to complete what I want to do?!

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Just because you have two loops doing the same number of iterations in different threads, does not mean they'll take the same length of time to complete. Generally, the contents of each iteration will take some amount of time to complete, and since your loops are doing different things, they'll (generally) take different lengths of time.

Further more, with threads in Python, The Global-Interpreter-Lock (GIL) blocks threads from running simultaneously on a multi-core CPU. The GIL is responsible for switching between threads, and continuing their execution, before switching to another, and then another, etc., etc. With QThreads, this becomes even more complex because Qt calls in a QThread can run without the GIL (so I understand) but general python code will still run with the GIL.

Because the GIL is responsible for handling what thread is running at any given time, I've even seen two threads, doing exactly the same thing, run at different speeds. As such, it is an entire coincidence that your two threads ever finished at the same time!.

Note, that because of the GIL, two cpu-instensive tasks will have no speed benefit from running in separate threads. For that, you need to use multi-processing. However, if you want to farm out I/O bound tasks (such as user interface through a GUI in the main thread, network communication in another thread, aka, tasks that often spend a lot of time waiting for something outside of the program to trigger something) then threading is useful.

So, hopefully that helps explain threading, and what is going on in your program.

There are a couple of ways to do this better. One would be to keep the loop in your thread, but remove the other. Then use the qt signal/slot mechanism to call a function in MainWindow which runs one iteration of the loop that used to be there. This doesn't guarantee synchronisation though, only that your QThread will finish first (something could slow down the main thread so that events pile up and the function in MainWindow doesn't run until later). To complete the synchronisation, you could use a threading.Event object to make the QThread wait until the new function in MainWindow has run.

Example (untested, sorry, but hopefully gives the idea!):

import threading
#==========================================
class TaskThread(QtCore.QThread):

    setTime = QtCore.pyqtSignal(int,int)

    iteration = QtCore.pyqtSignal(threading.Event, int)

    def run(self):

        self.setTime.emit(0,300)
        for i in range(300):
            time.sleep(0.05)
            event = threading.Event()
            self.iteration.emit(event, i)
            event.wait()

#==========================================
class MainWindow(QtGui.QMainWindow):

    _uiform = None

    def __init__(self, parent=None):
        QtGui.QMainWindow.__init__(self,parent)
        self._uiform = Ui_MainWindow()
        self._uiform.setupUi(self)
        self._uiform.runButton.clicked.connect(self.startThread)


    def startThread(self):
        self._uiform.progressBar.setRange(0,0)
        self.task = TaskThread()
        self.task.setTime.connect(self.changePB)
        self.task.iteration.connect(self.update_prog_bar)
        self.task.start()

    @QtCore.pyqtSlot(int,int)
    def changePB(self, c, t):
        self.proportionFinished = int(math.floor(100*(float(c)/t)))
        self._uiform.progressBar.setValue(self.proportionFinished)

        self._uiform.progressBar.setRange(0,300)
        self._uiform.progressBar.setValue(0)

    @QtCore.pyqtSlot(threading._Event,int)
    def update_prog_bar(self,event, i)
        self._uiform.progressBar.setValue(i+1)
        print i
        event.set()

Note the use of @QtCore.pyqtSlot() decorator is because of the issue documented here. In short, when you use signal.connect(my_function), you are omitting the second argument which determines the behaviour of the slot (whether it is executed immediately when signal.emit() is called, or whether it is executed once control returns to the event loop (aka, placed in a queue to be run later)). By default, this optional argument to connect tries to automatically decide which type of connection to make (see here) which works usually. However if the connection is made before it knows that it is a connection between threads, and the "slot" is **not* explicitly defined as a slot using @pyqtSlot, pyQT gets confused!

Additional info on decorators: The simplest way to think of decorators is a shorthand for wrapping a function in another function. The decorator replaces your defined function with its own, and this new function usually uses your original function at some point. So in the @pyqtSlot case, the signal emission actually calls a pyqt function generated by @pyqtSlot and this function eventually calls the original function you wrote. The fact that the @pyqtSlot decorator takes arguments that match the types of the arguments of your slot, is an implementation detail of the pyqt decorator, and not representative of decorators in general. It is simply stating that your slot expects to be passed data of the specified types by a connected signal.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...