如何在同时读取和写入文件时在 PyQt5 中正确执行多线程?

How do I do multithreading correctly in PyQt5 while reading and writing to a file simultaneously?

在一个 window 中,我有一个按钮,单击该按钮后,我想执行另一个模块的方法。此方法的执行时间不确定,具体取决于用户在终端中的输入。此方法创建一个文件并重复打开它,向文件写入内容,然后关闭文件。同时这是 运行ning 我在 window 中有一个 matplotlib 图形小部件,其中有一个绘图,每次通过读取和绘制来自最文件的最近行。

为了检查文件的更改,我正在使用 QFileSystemWatcher。现在,当 userInputFunction() 处于 运行ning 时没有任何反应,但是当它完成时我得到“data/runName_Rec.txt dataFileCreated”。如果我随后以任何方式手动编辑文件,绘图就会按预期进行。所以看起来观察者只是再次开始工作,并且在 userInputFunction() 完成后看到目录发生了变化。

如何正确执行多线程处理,以便在 userInputFunction() 处于 运行ning 时 watcher 工作?

据我了解,如果我在 QT 程序的主线程中 运行ning 完成用户输入功能,我的应用程序中的任何内容都不会响应。为了解决这个问题,我尝试按照此处的示例将用户输入方法的执行移动到工作线程中:https://realpython.com/python-pyqt-qthread/。在这个工人中,我有两种方法。一个简单地用 sleep() 做一个 for 循环,这需要一段时间,就像这个例子一样。其他 运行 是我的 userInputFunction()。 for 循环方法 运行() 不会冻结 GUI。但是, 运行Scan() 会触发我想要的实际过程,但它仍然会冻结 GUI。我不确定这里发生了什么。我不确定这是否意味着我没有正确进行穿线或者是否有其他问题。

这是我的代码相关部分的简化示例。

from PyQt5 import QtWidgets, uic, QtCore, QtGui
from pyqtgraph import PlotWidget
from PyQt5.QtCore import QObject, QThread, pyqtSignal
import pyqtgraph as pg
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QInputDialog, QLineEdit, QFileDialog, QMainWindow
import os
from os.path import exists
import csv
import numpy as np
import pandas as pd

import myModule

dirname = os.path.dirname(__file__)

# Create a worker class
class Worker(QObject):
    finished = pyqtSignal()

    #This works without freezing the GUI
    def run(self):
        """Long-running task."""
        for i in range(5):
            time.sleep(1)
            print("step ",i," done!")
        self.finished.emit()

    #This freezes the GUI
    def runScan(self,param1,param2):
        """Long-running task with user input from terminal."""
        myModule.userInputFunction(param1,param2)
        self.finished.emit()


class someWindow(QtWidgets.QMainWindow):

    def __init__(self, *args, **kwargs):
        super(someWindow, self).__init__(*args, **kwargs)

        #Load the UI Page
        uic.loadUi('somewindow.ui', self)

        self.directoryPath = "data"

        self.fs_watcher = QtCore.QFileSystemWatcher()

        self.fs_watcher.addPath(self.directoryPath)
        self.fs_watcher.directoryChanged.connect(self.dataFileCreated)
        
        self.StartScanButton.clicked.connect(self.runLongTask)
        self.EndScanButton.clicked.connect(self.endScan)

    def dataFileCreated(self):
        self.filePath = os.path.join(dirname, "data/"+ self.runNameBox.toPlainText()+"_Rec.txt")
        print(self.filePath + "dataFileCreated")
        self.fs_watcher.addPath(self.filePath)
        self.fs_watcher.fileChanged.connect(self.update_graph)

    def update_graph(self):
        if exists(self.path):
            print("file exists!")
            #then read the filePath.txt and plots the data
    else:
        print("file doesn't exist yet")

    def endScan(self):
        #change some display things

    def runLongTask(self):
        # Create a QThread object
        self.thread = QThread()
        # Create a worker object
        self.worker = Worker()
        # Move worker to the thread
        self.worker.moveToThread(self.thread)
        # Connect signals and slots
        #This results in the GUI freezing
        self.thread.started.connect(self.worker.runScan(self.param1,self.param2))
        #This DOES NOT result in the GUI freezing
        #self.thread.started.connect(self.worker.run)

        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        # Start the thread
        self.thread.start()

        # Final resets
        self.thread.finished.connect(
            lambda: print("long task done!")
        )

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    w = someWindow()
    w.show()
    sys.exit(app.exec_())

由于这一行,GUI 在您的案例中冻结:

self.thread.started.connect(self.worker.runScan(self.param1,self.param2))

这导致 runScan 从主线程执行并阻塞 任何东西 直到完成,包括 connect.

这也是一个严重的错误,因为 connect 总是期望一个可调用参数作为参数,一旦 runScan 最终完成它的工作,它 returns None 和你的程序会崩溃。

假设在创建线程时添加了参数,您可以将它们添加到 Worker 构造函数中,然后在 run:

中执行所需的代码
class Worker(QObject):
    finished = pyqtSignal()
    def __init__(self, param1, param2):
        super().__init__()
        self.param1 = param1
        self.param2 = param2

    def run(self):
        myModule.userInputFunction(self.param1, self.param2)
        self.finished.emit()


class someWindow(QtWidgets.QMainWindow):
    # ...
    def runLongTask(self):
        self.thread = QThread()
        self.worker = Worker(self.param1, self.param2)
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.run)

请注意,它不像 QFileSystemWatcher 在处理时“停止”:问题是通过 运行 runScan 在主线程中你完全阻塞了主线程(不仅是 UI), 阻止观察者处理 OS 发送给它以通知更改的事件。