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.