在 qthread 中停止长时间运行的进程

Posted

技术标签:

【中文标题】在 qthread 中停止长时间运行的进程【英文标题】:Stop long-running process in qthread 【发布时间】:2018-06-03 22:17:42 【问题描述】:

我正在尝试使用 youtube-dl 模块从 youtube 下载视频。我创建了一个简单的 GUI 来做一点工作,当用户单击开始按钮时,我需要调用线程并开始下载并使用 emit 方法发送数据,当此数据到达 Main 类中的 read 函数时,从GUI调用stop函数后线程必须停止,我尝试使用exec_()在qthread中创建事件循环并使用exit停止线程,我也尝试使用terminate但GUI冻结。

我使用的代码是:

from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from youtube_dl import *

class Worker(QThread):
    data = pyqtSignal(object)
    def __init__(self):
        super(Worker, self).__init__()
        self.flag = True

    def sendHook(self, data, status = 'status':'downloading'):
        self.data.emit(data)

    def stop(self):
        self.quit()
        self.exit()

    def run(self):
        self.y = YoutubeDL('progress_hooks':[self.sendHook])
        self.y.download(['https://www.youtube.com/watch?v=LKIXbNW-B5g'])
        self.exec_()

class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        layout = QVBoxLayout()
        self.l = QLabel("Hello")
        b = QPushButton("Start!")
        b.pressed.connect(self.connectthread)
        layout.addWidget(self.l)
        layout.addWidget(b)
        w = QWidget()
        w.setLayout(layout)
        self.setCentralWidget(w)
        self.show()

    def read(self, data):
        self.thread.stop()

    def connectthread(self):
        self.thread = Worker()
        self.thread.data.connect(self.read)
        self.thread.start()

app = QApplication([])
window = MainWindow()
app.exec_()

【问题讨论】:

【参考方案1】:

通过在工作线程的run() 方法中调用self.exec_(),您在下载完成后在这个线程上启动一个新的事件循环,然后这个事件循环继续运行。你在这里不需要一个事件循环,如果你想使用他们的moveToThread() 方法将 QObjects 移动到它,你只需要一个单独的事件循环,以将它们与主事件循环分离,但这里不需要,你是不做任何需要 Qt 事件循环的事情。这也是为什么调用stop()exit() 不会做任何事情,它只会影响事件循环。停止这个线程的唯一方法是它的terminate() 方法,这也很有效:

from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from youtube_dl import *

class Worker(QThread):
    data = pyqtSignal(object)
    def __init__(self):
        super(Worker, self).__init__()
        self.flag = True

    def sendHook(self, data, status = 'status':'downloading'):
        self.data.emit(data)

    def stop(self):
        self.terminate()
        print("QThread terminated")

    def run(self):
        self.y = YoutubeDL('progress_hooks':[self.sendHook])
        self.y.download(['https://www.youtube.com/watch?v=LKIXbNW-B5g'])
        print("finished")

class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        layout = QVBoxLayout()
        self.l = QLabel("Hello")
        b = QPushButton("Start!")
        b.pressed.connect(self.connectthread)
        layout.addWidget(self.l)
        layout.addWidget(b)
        w = QWidget()
        w.setLayout(layout)
        self.setCentralWidget(w)
        self.thread = None
        self.show()

    def read(self, data):
        print("read:", data)

    def connectthread(self):
        if self.thread is not None:
            # already running
            self.thread.stop()
            self.thread = None
            return
        self.thread = Worker()
        self.thread.data.connect(self.read)
        self.thread.start()

app = QApplication([])
window = MainWindow()
app.exec_()

在这里我已经改变了你的程序,所以第一次点击按钮时,worker 启动,第二次线程终止,依此类推。

但是,以这种方式终止线程是危险且不鼓励的。 Python 线程通常需要合作才能停止,因为按照设计它们没有办法被中断。在这种情况下,它之所以有效,是因为 PyQt 控制着线程。

很遗憾,没有办法优雅地停止 youtube-dl 下载,请参阅 this related issue 了解更多信息。一般来说,不能保证杀死调用download() 的线程实际上会停止下载。 YoutubeDL 支持具有不同下载器的插件系统。例如,要下载 hls 流,将启动一个外部 ffmpeg(或 avconv)进程,该进程不会通过终止工作线程来停止。对于在内部使用其他线程或进程的下载程序,或者也使用ffmpeg 执行的后处理步骤,情况也是如此。

如果您希望能够安全地停止下载,则必须使用单独的进程,以便使用SIGINT 信号(与按 Ctrl-C 相同)告诉它停止。

【讨论】:

以上是关于在 qthread 中停止长时间运行的进程的主要内容,如果未能解决你的问题,请参考以下文章

在 JDBC 中停止或终止长时间运行的查询

长时间运行的 py.test 在第一次失败时停止

如何停止长时间运行的 BigQuery 作业?

sqlalchemy:停止长时间运行的查询

什么可能导致长时间运行的进程中突然出现 ClassNotFoundException?

长时间运行的任务,我应该在另一个线程或另一个进程中运行它们吗? [复制]