2

I'm trying to update a pyqt QProgressBar from multiple threads, and from what I understand the best way to do this is by emitting signals back to the main GUI thread (I tried passing the QProgressBar object to the worker threads and though it did seem to work I got a ton of warnings in the interpreter). In the following code I set up a progressSignal signal and connect it to a thread which (for now) just prints whatever was emitted. I then emit from each thread the total percentage. I know this works outside the threads by just throwing out a random emit in line 47, which does come through. However the emit from line 36 does not trigger anything, so it never seems to make it through...

import Queue, threading
from PyQt4 import QtCore
import shutil
import profile

fileQueue = Queue.Queue()

class Communicate(QtCore.QObject):

    progressSignal = QtCore.pyqtSignal(int)

class ThreadedCopy:
    totalFiles = 0
    copyCount = 0
    lock = threading.Lock()

    def __init__(self, inputList, progressBar="Undefined"):
        self.totalFiles = len(inputList)

        self.c = Communicate()
        self.c.progressSignal.connect(self.updateProgressBar)

        print str(self.totalFiles) + " files to copy."
        self.threadWorkerCopy(inputList)


    def CopyWorker(self):
        while True:
            self.c.progressSignal.emit(2000)
            fileName = fileQueue.get()
            shutil.copy(fileName[0], fileName[1])
            fileQueue.task_done()
            with self.lock:
                self.copyCount += 1
                percent = (self.copyCount * 100) / self.totalFiles
                self.c.progressSignal.emit(percent)

    def threadWorkerCopy(self, fileNameList):

        for i in range(16):
            t = threading.Thread(target=self.CopyWorker)
            t.daemon = True
            t.start()
        for fileName in fileNameList:
            fileQueue.put(fileName)
        fileQueue.join()
        self.c.progressSignal.emit(1000)

    def updateProgressBar(self, percent):
        print percent

UPDATE:

Heres a sample with a gui. This one runs but is quite unstable, it crashes regularly and the UI does some weird stuff (progress bar not completing, etc.)

Main.py:

import sys, os
import MultithreadedCopy_5
from PyQt4 import QtCore, QtGui

def grabFiles(path):
    # gets all files (not folders) in a directory
    for file in os.listdir(path):
        if os.path.isfile(os.path.join(path, file)):
            yield os.path.join(path, file)

class MainWin(QtGui.QWidget):

    def __init__(self):
        super(MainWin, self).__init__()
        self.initUI()

    def initUI(self):
        self.progress = QtGui.QProgressBar()

        box = QtGui.QVBoxLayout()
        box.addWidget(self.progress)
        goBtn = QtGui.QPushButton("Start copy")
        box.addWidget(goBtn)

        self.setLayout(box)

        goBtn.clicked.connect(self.startCopy)

    def startCopy(self):
        files = grabFiles("folder/with/files")
        fileList = []
        for file in files:
            fileList.append([file,"folder/to/copy/to"])

        MultithreadedCopy_5.ThreadedCopy(fileList, self.progress)

def main():
    app = QtGui.QApplication(sys.argv)
    ex = MainWin()
    ex.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

MultithreadedCopy_5.py:

import Queue, threading
from PyQt4 import QtCore
import shutil
import profile

fileQueue = Queue.Queue()

class Communicate(QtCore.QObject):

    progressSignal = QtCore.pyqtSignal(int)

class ThreadedCopy:
    totalFiles = 0
    copyCount = 0
    lock = threading.Lock()

    def __init__(self, inputList, progressBar="Undefined"):
        self.progressBar = progressBar
        self.totalFiles = len(inputList)

        self.c = Communicate()
        self.c.progressSignal.connect(self.updateProgressBar, QtCore.Qt.DirectConnection)

        print str(self.totalFiles) + " files to copy."
        self.threadWorkerCopy(inputList)


    def CopyWorker(self):
        while True:
            fileName = fileQueue.get()
            shutil.copy(fileName[0], fileName[1])
            fileQueue.task_done()
            with self.lock:
                self.copyCount += 1
                percent = (self.copyCount * 100) / self.totalFiles
                self.c.progressSignal.emit(percent)

    def threadWorkerCopy(self, fileNameList):
        for i in range(16):
            t = threading.Thread(target=self.CopyWorker)
            t.daemon = True
            t.start()
        for fileName in fileNameList:
            fileQueue.put(fileName)
        fileQueue.join()

    def updateProgressBar(self, percent):
        self.progressBar.setValue(percent)

#profile.run('ThreadedCopy()')
Spencer
  • 1,931
  • 1
  • 21
  • 44
  • 1
    I'm starting to realize that python threads cannot emit signals back to the main pyqt app, so the "correct" way to do this would be with pyqt QThreads - which I would prefer not to do. Would there be an easy way to emit the signal in the main thread every time a worker thread finishes? – Spencer Aug 07 '17 at 23:41
  • Please see my answer for a solution which should at least fix the issues with the examples shown in your question. – ekhumoro Aug 10 '17 at 17:31

2 Answers2

3

There are two main problems with your examples.

Firstly, the object that emits the signals is created in the main/gui thread, so any signals its emits will not be cross-thread, and hence not thread-safe. The obvious solution to this is to create the signalling object inside the target function of the worker thread - which means there must be a separate instance for each thread.

Secondly, the while-loop inside the target function is never terminated, which means every ThreadedCopy object will be kept alive after the current copy operation is complete. Since all these objects share the same queue, the behaviour will become unpredictable if any attempt is made to repeat the copy operation. The obvious solution to this is to break out of the while-loop once the queue is empty.

Below is a re-write of MultithreadedCopy_5.py which should solve these issues. However, as stated in the comments, I would still strongly recommend using QThread rather than python threads in this scenario, as it is likely to provide a much more robust and more easily maintainable solution.

import Queue, threading
from PyQt4 import QtCore
import shutil
import profile

fileQueue = Queue.Queue()

class Communicate(QtCore.QObject):
    progressSignal = QtCore.pyqtSignal(int)

class ThreadedCopy:
    totalFiles = 0
    copyCount = 0
    lock = threading.Lock()

    def __init__(self, inputList, progressBar="Undefined"):
        self.progressBar = progressBar
        self.totalFiles = len(inputList)
        print str(self.totalFiles) + " files to copy."
        self.threadWorkerCopy(inputList)

    def CopyWorker(self):
        c = Communicate()
        c.progressSignal.connect(self.updateProgressBar)
        while True:
            try:
                fileName = fileQueue.get(False)
            except Queue.Empty:
                break
            else:
                shutil.copy(fileName[0], fileName[1])
                with self.lock:
                    self.copyCount += 1
                    percent = (self.copyCount * 100) / self.totalFiles
                    c.progressSignal.emit(percent)
                fileQueue.task_done()

    def threadWorkerCopy(self, fileNameList):
        if fileQueue.empty():
            for i in range(16):
                t = threading.Thread(target=self.CopyWorker)
                t.daemon = True
                t.start()
            for fileName in fileNameList:
                fileQueue.put(fileName)
            fileQueue.join()

    def updateProgressBar(self, percent):
        self.progressBar.setValue(percent)
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
-4

The main problem is the delay of time between sending the signal and receiving, we can reduce that time using processEvents():

You can call this function occasionally when your program is busy performing a long operation (e.g. copying a file).

def CopyWorker(self):
    while True:
        fileName = fileQueue.get()
        shutil.copy(fileName[0], fileName[1])
        fileQueue.task_done()
        with self.lock:
            self.copyCount += 1
            print(self.copyCount)
            percent = (self.copyCount * 100) / self.totalFiles
            self.c.progressSignal.emit(percent)
            QtCore.QCoreApplication.processEvents()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Perfect! Thanks, that's just what I needed. – Spencer Aug 08 '17 at 02:18
  • 2
    "The slot is executed in the signalling thread. " This doesn't sound good. Shouldn't GUI elements only be modified by the GUI thread? – NoDataDumpNoContribution Aug 08 '17 at 13:49
  • @Trilarion I answered the OP, the goal is not to run it in a GUI, so I proposed this answer. – eyllanesc Aug 08 '17 at 14:02
  • 3
    But according to the Qt docs such things must better be run in the GUI thread, that's why this solution should not be used, I guess. – NoDataDumpNoContribution Aug 08 '17 at 14:09
  • @Trilarion But according to the Qt docs such things must better be run in the GUI thread, that's why this solution should not be used, I guess. – eyllanesc Aug 08 '17 at 14:12
  • @Trilarion I have given a second alternative – eyllanesc Aug 08 '17 at 14:19
  • I did run into the situation that it wasn't running in the main GUI thread and gave me a warning (though it does run). I didn't down-vote because I wasn't sure if something else was wrong with my code - what would the proper solution be to do it in the main thread? The threading function is about 3 "layers" deep into my code if you get what I mean, and I'm not sure how to pass that signal all the way "up". – Spencer Aug 08 '17 at 21:14
  • @Spencer I have proposed a better solution, try it and tell me, I could give you a better opinion if you provide me a mvce of your program. – eyllanesc Aug 08 '17 at 21:17
  • @eyllanesc I see your edit, the solution you provided before worked fine and I'm not sure what this is solving? I wasn't having trouble with the main thread updating (which is what processEvents would solve), I just wasn't getting the signal at all - even when I tried to print it. Your earlier solution solved this. However both solutions have the problem trilarion mentioned, which I would like to know how to handle. I thought signal/slots would solve this but I guess I'm wrong. – Spencer Aug 08 '17 at 21:19
  • I guess in other words, how would I emit a signal back up through two or three functions to my main code block? Is that even correct? I'm not able to do it now but later today I'll mock up an example program to demonstrate what I'm running into. Thanks for the help btw! – Spencer Aug 08 '17 at 21:23
  • I do not understand much, I would like to see code to give my opinion. – eyllanesc Aug 08 '17 at 21:25
  • @eyllanesc Ok, created a sample. This isn't exactly what I have, but close. Interestingly it does not throw a warning, but does crash every other time I run it. – Spencer Aug 08 '17 at 23:18
  • @Spencer. What hardware are you planning to use this on? Parallel copying on a single hard-disk could very easily end up being *slower* than sequential copying - see [this superuser question for more details](https://superuser.com/q/252959). Have you actually timed your multi-threaded copying function and compared it with copying via a simple loop? – ekhumoro Aug 09 '17 at 13:49
  • @ekhumoro Im on extremely fast network storage with a ton of disks, you can see my results here: https://stackoverflow.com/questions/8584797/multithreaded-file-copy-is-far-slower-than-a-single-thread-on-a-multicore-cpu/45526392#45526392. I've been able to about 8x my copy speeds with 20 threads :) But yes after 20 threads it did slow down a bit. – Spencer Aug 09 '17 at 20:15
  • 1
    @Spencer. If you want this to work reliably and in a thread-safe way, you should use `QThread`. – ekhumoro Aug 10 '17 at 13:57