Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

PyQT5 QThread: The best way to access shared objects



  • Hi, I am having trouble understanding some stuff about QThread performance. I am developing small web cam app with PyQT5, in which I used separate Thread to capture images with cv2 library in infinite loop, and send it as a signal parameter to QMainWindow (GUI thread) to be displayed. When I do a resize of the main window, QMainWindow is sending a signal with new size as a parameter to the working thread, informing it to scale the taken images. Everything is working good, but I am wondering if I have implemented it in a safe way, because both GUI and worker thread are accessing same QSize variable (which is created in the main GUI thread) simultaneously. I don't know if I should put mutexes and where. Here is the code that I created.

    View class:

    class View(QtWidgets.QMainWindow):
      reSize = pyqtSignal(QSize)
      def __init__(self):
          super(View,self).__init__()
          uic.loadUi('ui/mainwindow.ui',self)
          self.canvas_label  = self.findChild(QtWidgets.QLabel, 'canvas')
          self.backgroundWorker = BackgroundWorker(self.canvas_label.size())
          self.backgroundWorker.changePixmap.connect(self.setImage)
          self.reSize.connect(self.backgroundWorker.scaled)
          self.backgroundWorker.start()
    
      @pyqtSlot(QImage)
      def setImage(self, image):
          self.canvas_label.setPixmap(QPixmap.fromImage(image))
    
      def resizeEvent(self, event):
        self.reSize.emit(self.findChild(QtWidgets.QLabel, 'canvas').size())
    

    BackgroundWorker:

    class BackgroundWorker(QThread):
    
        changePixmap = pyqtSignal(QImage)
        
        def __init__(self,img_dimensions):
            super().__init__()
            self.img_dimensions = img_dimensions
            self.mutex = QMutex()
    
        def run(self):
            cap = cv2.VideoCapture(0)
            while True:
                ret, frame = cap.read()
                if ret:
                    rgbImage = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                    h, w, ch = rgbImage.shape
                    bytesPerLine = ch * w
                    convertToQtFormat = QImage(rgbImage.data, w, h, bytesPerLine, QImage.Format_RGB888)
                    p = convertToQtFormat.scaled(self.img_dimensions.width(), self.img_dimensions.height())
                    self.changePixmap.emit(p)
    
        def scaled(self, scaled_size):
            self.img_dimensions = scaled_size
    

    I was wondering would it be correct to create QMutex in Background worker constructor, and use it to lock like this:

     self.mutex.lock()
     p = convertToQtFormat.scaled(self.img_dimensions.width(), self.img_dimensions.height())
     self.mutex.unlock()
    

    in run() function, and in scaled function like this:

     def scaled(self, scaled_size):
          self.mutex.lock()
          self.img_dimensions = scaled_size
          self.mutex.unlock()
    

  • Lifetime Qt Champion

    @mitaa257 said in PyQT5 QThread: The best way to access shared objects:

    same QSize variable

    Which size variable?
    As far as I can see you do this:

    self.reSize.emit(self.findChild(QtWidgets.QLabel, 'canvas').size())
    

    Which should be safe as for queued connections the parameters are copied if I'm not mistaken. So you are not accessing anything shared.
    See https://doc.qt.io/qt-5/qt.html#ConnectionType-enum
    "With queued connections, the parameters must be of types that are known to Qt's meta-object system, because Qt needs to copy the arguments to store them in an event behind the scenes."



  • If I understood it good (according to this source https://wiki.qt.io/Threads_Events_QObjects) this is not queued connection, but a direct one, because in the run() method we got endless loop, so the thread's event loop could never process the event. I was referring to the variable img_dimensions in BackgroundWorker class. I think it is accessed in the run method by a worker thread, and trough a scaled function too.



  • @mitaa257 said in PyQT5 QThread: The best way to access shared objects:

    If I understood it good (according to this source https://wiki.qt.io/Threads_Events_QObjects) this is not queued connection, but a direct one

    Why do you say this? The signalling thread is different from the slot one, so the default will be queued. It is the connect() which implements this, and that does not know whether the thread does or doesn't have an event loop. The event loop is required in the slot thread to receive it (for queued), but I don't think there is any requirement in the emitting thread to have an event loop(?) (Unless perchance PyQt requires that, but I don't think Qt does.)



  • I think that just the stuff that happens inside run() function is happening in other thread, look at the examples in the link that I provided. I don't think that the signaling thread and the slot one, in this case, are different, because they have the same thread affinity. How is it possible for resize event to get processed in BackgroundWorker slot, if these two threads are different, and BackroundWorker is stuck in endless loop ?


  • Lifetime Qt Champion

    Hi,

    You are correct, since you subclassed QThread, the slots are called in the main thread but the run method in the new thread.

    There are different strategies that can be used to avoid the overhead of a mutex on each loop. You might want to consider a dirty flag so you only read the new value once after is has changed. You still protect the access to your size variable but you access it less often.


Log in to reply