Как приостановить цикл в потоке другим потоком в PyQt

#python #multithreading #pyqt #signals #mutex

#python #многопоточность #pyqt #сигналы #мьютекс

Вопрос:

Я пытаюсь написать простой виджет PyQt, который использует двух рабочих. Первый рабочий (рабочий 1) немедленно запускает свой цикл, в то время как второй (рабочий 2) спит в течение 2 секунд, а затем запускается. Я хотел бы получить следующее:

  1. Рабочий 2 приостанавливает выполнение рабочего 1
  2. Рабочий 2 запускает и завершает цикл
  3. Рабочий 2 завершается и позволяет рабочему 1 завершить свой собственный цикл.

Я попытался добиться этого, реализовав следующий код для рабочего класса:

 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.")

  

В то время как виджет и выполнение:

 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_())
  

Что я хотел бы видеть в терминале, так это:

 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.
  

Этого не происходит, потому что рабочий 2 подает сигнал для снятия блокировки «самому себе» и не может отправить его рабочему 1. Это означает, что два процесса не синхронизированы. Кроме того, программа завершает работу в конце цикла Worker 1 (предположительно, потому, что блокировка уже снята при перезапуске Worker 1).

Что я должен сделать, чтобы заставить код работать желаемым образом?

Ответ №1:

В документации указано, что:

Когда вы вызываете lock() в потоке, другие потоки, которые пытаются вызвать lock() в том же месте, будут блокироваться до тех пор, пока поток, получивший блокировку, не вызовет unlock() .

Итак, единственный поток, которому разрешено разблокировать, — это поток Worker1.

Из вашего описания того, чего вы хотите достичь, я думаю, что lock() — неправильный подход. Блокировка «защищает» фрагмент кода, так что только один поток может получить к нему доступ одновременно.

Если вы все еще хотите использовать этот механизм блокировки, я бы предложил следующий шаблон:

Добавьте флаг «is_blocking», который определяет, должен ли рабочий получать блокировку в течение всего срока службы. Если рабочий не блокируется, он периодически пытается заблокировать и разблокировать. Если блокирующий рабочий активен, это приводит к блокировке поведения.

Вот примерная демонстрация, основанная на вашем примере:

 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_())