简体   繁体   中英

How to pause loop in thread with another thread in PyQt

I am trying to write a simple PyQt widget that uses two workers. The first worker (Worker 1) starts its loop immediately, while the second one (Worker 2) sleeps for 2 seconds and then starts. I would like to get the following:

  1. Worker 2 pauses Worker 1 execution
  2. Worker 2 starts and completes the loop
  3. Worker 2 finishes and lets Worker 1 complete its own loop.

I have tried to get this by implementing the following code for the worker class:

class Worker(QObject):
    """
    Create worker class
    """
    worker_finished = pyqtSignal()
    ask_to_work = pyqtSignal()
    glob_lock = QMutex()

    def __init__(self, name, sleep_time = 0.5, loops = 10, sleep_at_start=False):
        super().__init__()
        self._name = name
        self.sleep_time = sleep_time
        self.loops = loops
        self._sleep_at_start = sleep_at_start
        self.worker_finished.connect(self.at_finish)
        self.ask_to_work.connect(self.pause_execution)

    @pyqtSlot()
    def run(self):
        #This is to make the worker sleep 2 seconds, then ask to run its loop
        if self._sleep_at_start:
            print("{:s} is sleeping.".format(self._name))
            sleep(2)
            print("{:s} has awaken.".format(self._name))
            self.ask_to_work.emit()

        #This acquires the lock and starts the worker loop
        self.glob_lock.lock()
        print("Lock acquired by {:s}".format(self._name))
        for ii in range(self.loops):
            print("I am {:s} and I am at step {:d}".format(self._name, ii))
            sleep(self.sleep_time)
        self.worker_finished.emit()

    #Function to attempt the interruption of the worker loop
    @pyqtSlot()
    def pause_execution(self):
        try:
            print("Request lock")
            self.glob_lock.unlock()
            print("Request succeded")
        except:
            print("No lock to release")

    @pyqtSlot()
    def at_finish(self):
        try:
            self.glob_lock.unlock()
            print("Successfully released lock at finish.")
        except:
            print("Releasing the lock at finish went wrong.")

While widget and the execution are:

class MainApp(QWidget):
    """
    Make the widget
    """
    def __init__(self):
        super().__init__()
        #Set the widget layout
        layout = QHBoxLayout(self)

        self.button_run = QPushButton("Run workers")
        layout.addWidget(self.button_run)

        self._threads = []
        self._workers = []

        #Initialize the workers
        worker = Worker("Worker 1", loops=10)
        self._workers.append(worker)

        worker = Worker("Worker 2", sleep_time=1,
                        loops=1, sleep_at_start=True)
        self._workers.append(worker)

        #Connect workers' run methods to the widget button and put the workers into separate threads
        self.activate_and_connect_workers()

    def activate_and_connect_workers(self):
        for worker in self._workers:
            self.button_run.clicked.connect(worker.run)
            a_thread = QThread()
            self._threads.append(a_thread)
            worker.moveToThread(a_thread)
            a_thread.start()

if __name__ == '__main__':

    app = QApplication(sys.argv)
    window = MainApp()
    window.show()
    sys.exit(app.exec_())

What I would like to see in the terminal is:

Lock acquired by Worker 1
Worker 2 is sleeping
I am Worker 1 and I am at step 0
I am Worker 1 and I am at step 1
#...Some lines of Worker 1
Worker 2 has awaken.
Request lock
Request succeded
Lock acquired by Worker 2
I am Worker 2 and I am at step 0
#...Some lines of Worker 2
Worker 2 uccessfully released lock at finish.
Lock acquired by Worker 1
#...More lines of Worker 1
Worker 1 uccessfully released lock at finish.

This does not happen, because Worker 2 emits the signal to release the lock "to itself" and is not able to send it to Worker 1. This means the two processes are not synchronized. On top of that, the program crashes at the end of the Worker 1 loop (presumably because the lock is already released when Worker 1 restarts).

What should I do to make the code work in the desired way?

The documentation states that:

When you call lock() in a thread, other threads that try to call lock() in the same place will block until the thread that got the lock calls unlock().

So, the only thread who is allowed to unlock is the thread of Worker1.

From your description of what you want to achieve, I think that lock() is the wrong approach. Locking "protects" a piece of code, so that only one thread can access it at a time.

If you still want do use this a blocking mechanism, I would suggest the following pattern:

Add an "is_blocking" flag which determines whether the worker should get the lock along its lifetime. If the worker is not blocking, it periodically tries to lock and unlock. If a blocking Worker is active, this leads to a blocking behaviour.

Here's a rough demo, based on your example:

import sys
from PyQt5.QtCore import QObject, pyqtSignal, QMutex, pyqtSlot, QThread, Qt
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QPushButton, QApplication
from time import sleep

glob_lock = QMutex()

class Worker(QObject):
    worker_finished = pyqtSignal()

    def __init__(self, name, sleep_time = 0.5, loops = 10, sleep_at_start=False, is_blocking=False):
        super().__init__()
        self._name = name
        self.sleep_time = sleep_time
        self.loops = loops
        self._sleep_at_start = sleep_at_start
        self.worker_finished.connect(self.at_finish)
        self.is_blocking = is_blocking

    @pyqtSlot()
    def run(self):
        if self._sleep_at_start:
            print("{:s} is sleeping.".format(self._name))
            sleep(2)
            print("{:s} has awaken.".format(self._name))
        if self.is_blocking:
            glob_lock.lock()
            print("Lock acquired by {:s}".format(self._name))
        for ii in range(self.loops):
            if not self.is_blocking:
                glob_lock.lock()
                glob_lock.unlock()
            print("I am {:s} and I am at step {:d}".format(self._name, ii))
            sleep(self.sleep_time)
        self.worker_finished.emit()


    @pyqtSlot()
    def at_finish(self):
        if self.is_blocking:
            glob_lock.unlock()
            print("Successfully released lock at finish.")
        else:
            print("Successfully finished.")

class MainApp(QWidget):
    def __init__(self):
        super().__init__()
        layout = QHBoxLayout(self)
        self.button_run = QPushButton("Run workers")
        layout.addWidget(self.button_run)

        self._threads = []
        self._workers = []

        worker = Worker("Worker 1", loops=10)
        self._workers.append(worker)
        worker = Worker("Worker 2", sleep_time=1, loops=5, sleep_at_start=True, is_blocking=True)
        self._workers.append(worker)
        self.activate_and_connect_workers()

    def activate_and_connect_workers(self):
        for worker in self._workers:
            self.button_run.clicked.connect(worker.run)
            a_thread = QThread()
            self._threads.append(a_thread)
            worker.moveToThread(a_thread)
            a_thread.start()

if __name__ == '__main__':

    app = QApplication(sys.argv)
    window = MainApp()
    window.show()
    sys.exit(app.exec_())

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM