title: PyQT - QThread.sleep(...) in separate thread blocks UI
body: <p>First I'll give a short description of the user interface and its functions before diving into the problem and code. Sorry in advance but I am unable to provide the complete code (even if I could it's...a lot of lines :D).</p>
  
  <p><strong>Description of the UI and what it does</strong>
  I have a custom <code>QWidget</code> or to be more precise N instances of that custom widget aligned in a grid layout. Each instance of the widget has its own <code>QThread</code> which holds a worker <code>QObject</code> and a <code>QTimer</code>. In terms of UI components the widget contains two important components - a <code>QLabel</code> which visualizes a status and a <code>QPushButton</code>, which either starts (by triggering a <code>start()</code> slot in the Worker) or stops (by triggering a <code>slot()</code> slot in the worker) an external process. Both slots contain a 5s delay and also disable the push button during their execution. The worker itself not only controls the external process (through calls to the two slots mentioned above) but also checks if the process is running by a <code>status()</code> slot, which is triggered by the <code>QTimer</code> every 1s. As mentioned both the worker and timer live inside the thread! (I have double-checked that by printing the thread ID of the main (where the UI is) and the one of each worker (different from the main 100% sure).</p>
  
  <p>In order to reduce the amount of calls from the UI to the worker and vice versa I decided to declare the <code>_status</code> attribute (which hold the state of the external process - <strong>inactive</strong>, <strong>running</strong>, <strong>error</strong>) of my <code>Worker</code> class as <code>Q_PROPERTY</code> with a <code>setter</code>, <code>getter</code> and <code>notify</code> the last being a signal triggered from within the <code>setter</code> IF and only if the value has changed from the old one. My previous design was much more signal/slot intensive since the status was emitted literally every second.</p>
  
  <p>Now it's time for some code. I have reduced the code only to the parts which I deem to provide enough info and the location where the problem occurs:</p>
  
  <hr>
  
  <p><strong>Inside the QWidget</strong></p>
  
  <pre><code># ...
  
  def createWorker(self):
      # Create thread
      self.worker_thread = QThread()
  
      # Create worker and connect the UI to it
      self.worker = None
      if self.pkg: self.worker = Worker(self.cmd, self.pkg, self.args)
      else: self.worker = Worker(cmd=self.cmd, pkg=None, args=self.args)
  
      # Trigger attempt to recover previous state of external process
      QTimer.singleShot(1, self.worker.recover)
  
      self.worker.statusChanged_signal.connect(self.statusChangedReceived)
      self.worker.block_signal.connect(self.block)
      self.worker.recover_signal.connect(self.recover)
      self.start_signal.connect(self.worker.start)
      self.stop_signal.connect(self.worker.stop)
      self.clear_error_signal.connect(self.worker.clear_error)
  
      # Create a timer which will trigger the status slot of the worker every 1s (the status slot sends back status updates to the UI (see statusChangedReceived(self, status) slot))
      self.timer = QTimer()
      self.timer.setInterval(1000)
      self.timer.timeout.connect(self.worker.status)
  
      # Connect the thread to the worker and timer
      self.worker_thread.finished.connect(self.worker.deleteLater)
      self.worker_thread.finished.connect(self.timer.deleteLater)
      self.worker_thread.started.connect(self.timer.start)
  
      # Move the worker and timer to the thread...
      self.worker.moveToThread(self.worker_thread)
      self.timer.moveToThread(self.worker_thread)
  
      # Start the thread
      self.worker_thread.start()
  
  @pyqtSlot(int)
    def statusChangedReceived(self, status):
      '''
      Update the UI based on the status of the running process
      :param status - status of the process started and monitored by the worker
      Following values for status are possible:
        - INACTIVE/FINISHED - visual indicator is set to INACTIVE icon; this state indicates that the process has stopped running (without error) or has never been started
        - RUNNING - if process is started successfully visual indicator
        - FAILED_START - occurrs if the attempt to start the process has failed
        - FAILED_STOP - occurrs if the process wasn't stop from the UI but externally (normal exit or crash)
      '''
      #print(' --- main thread ID: %d ---' % QThread.currentThreadId())
      if status == ProcStatus.INACTIVE or status == ProcStatus.FINISHED:
        # ...
      elif status == ProcStatus.RUNNING:
        # ...
      elif status == ProcStatus.FAILED_START:
        # ...
      elif status == ProcStatus.FAILED_STOP:
        # ...
  
  @pyqtSlot(bool)
    def block(self, block_flag):
      '''
      Enable/Disable the button which starts/stops the external process
      This slot is used for preventing the user to interact with the UI while starting/stopping the external process after a start/stop procedure has been initiated
      After the respective procedure has been completed the button will be enabled again
      :param block_flag - enable/disable flag for the button
      '''
      self.execute_button.setDisabled(block_flag)
  
  
  # ...
  </code></pre>
  
  <hr>
  
  <p><strong>Inside the Worker</strong></p>
  
  <pre><code># ...
  
    @pyqtSlot()
    def start(self):
      self.block_signal.emit(True)
      if not self.active and not self.pid:
        self.active, self.pid = QProcess.startDetached(self.cmd, self.args, self.dir_name)
        QThread.sleep(5)
  
        # Check if launching the external process was successful
        if not self.active or not self.pid:
          self.setStatus(ProcStatus.FAILED_START)
          self.block_signal(False)
          self.cleanup()
          return
  
        self.writePidToFile()
        self.setStatus(ProcStatus.RUNNING)
  
      self.block_signal.emit(False)
  
    @pyqtSlot()
    def stop(self):
      self.block_signal.emit(True)
      if self.active and self.pid:
          try:
            kill(self.pid, SIGINT)
            QThread.sleep(5) # &lt;----------------------- UI freezes here
          except OSError:
            self.setStatus(ProcStatus.FAILED_STOP)
  
      self.cleanup()
      self.active = False
      self.pid = None
      self.setStatus(ProcStatus.FINISHED)
      self.block_signal.emit(False)
  
    @pyqtSlot()
    def status(self):
      if self.active and self.pid:
        running = self.checkProcessRunning(self.pid)
        if not running:
          self.setStatus(ProcStatus.FAILED_STOP)
          self.cleanup()
          self.active = False
          self.pid = None
  
    def setStatus(self, status):
      if self._status == status: return
      #print(' --- main thread ID: %d ---' % QThread.currentThreadId())
      self._status = status
      self.statusChanged_signal.emit(self._status)
  </code></pre>
  
  <hr>
  
  <p><strong>And now about my problem:</strong> I have noticed that the UI freezes ONLY whenever the <code>stop()</code> slot is triggered and the execution of the code goes through the <code>QThread.sleep(5)</code>. I thought that this should also affect start but with multiple instances of my widget (each controlling its own thread with a worker and timer living in it) and all of that running the start works as intended - the push button, which is used to trigger the <code>start()</code> and <code>stop()</code> slots, gets disabled for 5 seconds and then gets enabled. With the <code>stop()</code> being triggered this doesn't happen at all.</p>
  
  <p>I really cannot explain this behaviour. What's even worse is that the status updates that I am emitting through the <code>Q_PROPERTY</code> setter <code>self.setStatus(...)</code> get delayed due to this freezing which leads to some extra calls of my <code>cleanup()</code> function which basically deletes a generated file.</p>
  
  <p>Any idea what is going on here? The nature of a slot and signal is that once a signal is emitted the slot connected to it is called right away. And since the UI runs in a different thread then the worker I don't see why all this is happening.</p>