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

Posted

技术标签:

【中文标题】如何在 PyQt5 中同时读取和写入文件时正确执行多线程?【英文标题】:How do I do multithreading correctly in PyQt5 while reading and writing to a file simultaneously? 【发布时间】:2021-10-17 22:40:35 【问题描述】:

在一个窗口中,我有一个按钮,单击该按钮后,我想从另一个模块执行一个方法。此方法执行时间不确定,并且取决于终端中的用户输入。此方法创建一个文件并重复打开它,将内容写入文件,然后关闭文件。在此运行的同时,我在窗口中有一个 matplotlib 图形小部件,每次通过读取和绘制文件最新行中的数据将新内容写入文件时,我想更新一个绘图。

为了检查文件的更改,我使用了 QFileSystemWatcher。现在,在 userInputFunction() 运行时没有任何反应,但是当它完成时,我得到“data/runName_Rec.txt dataFileCreated”。如果我随后以任何方式手动编辑文件,则绘图会按原样进行。因此,观察者似乎只是重新开始工作,并在 userInputFunction() 完成后看到目录发生了变化。

如何正确执行多线程,以便观察者在 userInputFunction() 运行时工作?

据我了解,如果我让它在我的 QT 程序的主线程中运行,我的应用程序中的任何内容都不会响应,直到用户输入函数完成。为了解决这个问题,我尝试按照此处的示例将用户输入法的执行移动到工作线程中:https://realpython.com/python-pyqt-qthread/。在这个工人中,我有两种方法。一个简单的使用 sleep() 的 for 循环,它需要一段时间,就像示例一样。另一个运行我的 userInputFunction()。 for 循环方法 run() 不会冻结 GUI。但是,执行我想要的实际过程的 runScan() 仍然会冻结 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_())

【问题讨论】:

【参考方案1】:

由于这一行,GUI 在您的情况下冻结:

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

这会导致runScan 从主线程执行并阻塞任何事情直到完成,包括connect

这也是一个严重的错误,因为connect 总是希望有一个可调用的参数,一旦runScan 最终完成它的工作,它就会返回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),从而阻止观察者处理操作系统发送它以通知更改。

【讨论】:

这工作哇!谢谢!

以上是关于如何在 PyQt5 中同时读取和写入文件时正确执行多线程?的主要内容,如果未能解决你的问题,请参考以下文章

如何同时读取和写入数据到同一个文件

如何同时从 grpc 流中读取和写入

如何在从 NodeJS 中的多个输入流中读取时写入单个文件

Asp net core 在运行时读取或写入视图

读取 xls,将所有日期转换为正确格式,-> 写入 csv

每次另一个另一个进程更新文件时如何从文件中读取